Skip to content

fix: lib-led DDP stream restoration on Zombie Connection#7513

Open
diegolmello wants to merge 20 commits into
developfrom
mirage-laborer
Open

fix: lib-led DDP stream restoration on Zombie Connection#7513
diegolmello wants to merge 20 commits into
developfrom
mirage-laborer

Conversation

@diegolmello

@diegolmello diegolmello commented Jul 24, 2026

Copy link
Copy Markdown
Member

Proposed changes

A failed resume-login could leave the app with a live DDP socket that is connected but not authenticated and holding zero subscriptions — a Zombie Connection. In that state streams never come back until the app is restarted: no new messages, no room-list updates.

Recovery now lives in the SDK's own resubscribe path plus two small app-side gates, with no separate restoration owner:

  • SDK patchforceReopen no longer wipes the subscription registry, so the next login replays every subscription via subscribeAll(). A synchronous (name, params) idempotency guard dedups a replayed subscription against one the app re-issues, and eager-send entries are normalized with onEvent.
  • connect.ts — the connected stream listener early-returns only when the session is already authenticated, so a Zombie Connection re-runs resume-login instead of stranding.
  • state.js — on foreground, when inside the authenticated root with a token present but not authenticated, it dispatches a resume login.
  • room.ts — drop the teardown of the prior room subscriptions in handleLogin; the fresh DDP session already dropped them server-side and the SDK dedups the re-subscribe.

The reported symptom is two separate defects

Verifying the original report on an Android emulator showed that the user-visible "message is missing until I clear the cache" symptom is caused by two independent bugs, and this PR fixes only the first:

  1. Zombie Connection — the socket stays connected but unauthenticated with no subscriptions, so streams are dead. Fixed here. After this change, foregrounding recovers the connection in both the notification-tap and app-icon paths: authenticated again, subscriptions back, and live messages arriving in about a second.
  2. Offline gap — a message that was sent while the client was disconnected is never backfilled once the connection recovers, leaving a permanent hole in the room even though the server holds the message. That is a sync-cursor bug, fixed by the advanceSyncCursor change in fix: derive message sync cursor from server timestamps #7506.

With this PR alone the connection recovers but the missed message stays missing. With #7506 underneath it, the reported repro stops reproducing — the missed message arrives shortly after foreground, exactly once, in correct chronological order. This PR is therefore stacked on #7506 and the two are meant to land together.

Note for installs that already hit the bug

#7506 prevents new gaps; it does not repair gaps that already exist locally. Rooms that were gapped before upgrading split in two:

  • If the room's lastOpen is unset, simply opening the room refetches the missing messages and the room heals.
  • If lastOpen was already stamped past the missing window, nothing in the app will re-request it — neither re-opening the room nor restarting the app heals it. Those users keep the hole until local storage is cleared.

No migration is included to repair the second case.

Issue(s)

How to test or reproduce

  1. Log in to a server and open a room.
  2. Drive the client into a Zombie Connection: force the socket to reopen while the resume login fails once (e.g. background the app long enough for the socket to die, then foreground), so it reconnects connected-but-unauthenticated.
  3. Confirm streams recover on their own — new messages arrive, the room list updates, presence works — without restarting the app.
  4. To check the full reported repro, keep fix: derive message sync cursor from server timestamps #7506 underneath: send a message from another account while the client is disconnected, then foreground the client and confirm the message appears in the right position without clearing the cache.

Screenshots

N/A — no UI changes.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

The fix is deliberately subtractive: rather than adding a restoration owner that races the SDK, it lets the SDK own resubscribe and removes the app-side code that was fighting it. The one addition is the idempotency guard that makes replay safe against a concurrent app re-subscribe.

This PR is stacked on #7506, so its base is that branch rather than develop; review and merge it after #7506.

Summary by CodeRabbit

  • Bug Fixes
    • Improved “zombie” connection recovery by resuming authentication when needed, preventing relogin storms on healthy connections.
    • Restored room subscriptions across reconnects without tearing down stale ones; ensured subscription replay is deduplicated and consistent.
    • Hardened reconnect and message sync, including liveness probing and more accurate cursor advancement for out-of-order updates and clock-skew scenarios.
  • Documentation
    • Expanded glossary with definitions for zombie connections and resubscription behavior.
  • Tests
    • Added/expanded Jest and integration coverage for reconnect/resubscribe, socket subscription restoration, and sync cursor recovery (including null cursor and cursor poisoning).

A failed resume-login could strand a connected socket that is not
authenticated with zero subscriptions (a Zombie Connection). Recovery
now lives in the SDK's own resubscribe path plus two surgical app-side
gates, with no separate restoration owner.

- SDK patch: forceReopen no longer wipes the subscription registry, so
  the next login replays every sub via subscribeAll(); add a synchronous
  (name, params) idempotency guard so replay and app re-issue dedup, and
  normalize eager-send entries with onEvent.
- connect.ts: the connected listener early-returns only when already
  authenticated, so a Zombie Connection re-runs resume-login.
- state.js: on foreground inside ROOT_INSIDE with a token but no auth,
  dispatch loginRequest({ resume }).
- room.ts: drop the teardown of prior subscriptions in handleLogin; the
  fresh DDP session already dropped them and the SDK dedups the re-sub.
@diegolmello
diegolmello temporarily deployed to approve_e2e_testing July 24, 2026 16:16 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR updates DDP reconnection and subscription replay, adds zombie-connection resume handling, changes room subscription cleanup, introduces server-derived sync cursor advancement, adds LokiJS integration coverage, and replaces room initialization retry timeouts with a capped backoff scheduler.

Changes

Connection and subscription recovery

Layer / File(s) Summary
DDP socket replay and reconnect handling
patches/@rocket.chat+sdk+1.3.3-mobile.patch, app/lib/services/ddpSocket.test.ts
Socket reopen preserves subscriptions, probes stale connections, ignores orphan close events, deduplicates identical subscriptions, and replays preserved entries.
Zombie connection resume flow
app/lib/services/connect.ts, app/lib/services/connect.test.ts, app/sagas/state.js, app/sagas/__tests__/state.test.ts, CONTEXT.md
Connected-but-unauthenticated sessions dispatch resume login using the available token, while authenticated sessions avoid redundant resume actions.
Room subscription recovery
app/lib/methods/subscriptions/room.ts, app/lib/methods/subscriptions/room.test.ts, app/lib/methods/readMessages.ts, app/views/RoomView/index.tsx
Login recovery stops explicitly unsubscribing prior tracked room subscriptions, and read operations no longer update the server synchronization cursor.
Server-derived synchronization cursor
app/lib/methods/helpers/advanceSyncCursor.ts, app/lib/methods/loadMessagesForRoom.ts, app/lib/methods/loadMissedMessages.ts, app/lib/database/services/Message.ts
Room cursors advance from server _updatedAt values after successful message loading, with bounded updated/deleted pagination and forward-only persistence guards.
Database test harness and integration coverage
app/lib/database/__tests__/*, jest.config.js
Adds LokiJS database helpers, seeded records, a paginated fake sync server, and integration tests for cursor fallback, persistence, reconnect cycles, deletions, failures, and concurrency.
Room initialization retry scheduling
app/views/RoomView/services/initRetryScheduler.ts, app/views/RoomView/services/initRetryScheduler.test.ts, app/views/RoomView/index.tsx
Room initialization retries use capped exponential backoff with reset, cancellation, and single-pending-retry behavior.
Patch diff handling
.gitattributes
Patch files are classified as text and diffed as text.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels: type: bug

Suggested reviewers: otaviostasiak, rohit3523

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: restoring DDP streams during Zombie Connection recovery.
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.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • SUP-1078: Request failed with status code 401

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.

@diegolmello
diegolmello temporarily deployed to approve_e2e_testing July 24, 2026 16:25 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
patches/@rocket.chat+sdk+1.3.3-mobile.patch (2)

198-217: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Callback silently dropped if existing.onEvent is falsy.

if (existing) {
  if (callback && existing.onEvent) existing.onEvent(callback)
  return Promise.resolve(existing)
}

If existing.onEvent is ever missing, the caller's callback is silently discarded — no error, no log, and the caller believes it subscribed successfully. Currently unreachable because every write path to this.subscriptions now sets onEvent (line 191, line 238), but it's worth a defensive log if this branch is ever hit, since a silently-dropped message callback is hard to diagnose.

🤖 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 `@patches/`@rocket.chat+sdk+1.3.3-mobile.patch around lines 198 - 217, Update
the existing-subscription branch in subscribe so a provided callback is not
silently discarded when existing.onEvent is unavailable. Preserve the current
callback attachment when existing.onEvent exists, and add defensive diagnostic
logging for the missing-handler case before returning the resolved existing
subscription.

152-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for the new checkAndReopen/probe bucket logic.

This three-bucket decision tree (stale/fresh/gray+probe) is new, non-trivial branching logic with hardcoded timing thresholds (ping*2, 2000, probe's 2s timeout), yet ddpSocket.test.ts only covers subscription replay/dedup, not checkAndReopen/probe. A regression here (e.g., wrong bucket boundary) would silently degrade reconnect reliability.

Want me to draft unit tests for the three buckets (stale, fresh, gray-probe-pass, gray-probe-fail)?

🤖 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 `@patches/`@rocket.chat+sdk+1.3.3-mobile.patch around lines 152 - 182, Add unit
coverage in ddpSocket.test.ts for checkAndReopen and probe, covering
disconnected or stale connections forcing reopen, fresh traffic returning
without probing, gray-window probe success preserving the connection, and probe
failure forcing reopen. Assert the boundary behavior for ping*2 and 2000ms
thresholds, and mock timing, probe responses, and forceReopen so each bucket’s
action is verified.
🤖 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.

Nitpick comments:
In `@patches/`@rocket.chat+sdk+1.3.3-mobile.patch:
- Around line 198-217: Update the existing-subscription branch in subscribe so a
provided callback is not silently discarded when existing.onEvent is
unavailable. Preserve the current callback attachment when existing.onEvent
exists, and add defensive diagnostic logging for the missing-handler case before
returning the resolved existing subscription.
- Around line 152-182: Add unit coverage in ddpSocket.test.ts for checkAndReopen
and probe, covering disconnected or stale connections forcing reopen, fresh
traffic returning without probing, gray-window probe success preserving the
connection, and probe failure forcing reopen. Assert the boundary behavior for
ping*2 and 2000ms thresholds, and mock timing, probe responses, and forceReopen
so each bucket’s action is verified.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1b88ac6-08ef-4a28-84f8-c265f30a6d78

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9b5cc and 849e846.

📒 Files selected for processing (9)
  • CONTEXT.md
  • app/lib/methods/subscriptions/room.test.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/services/connect.test.ts
  • app/lib/services/connect.ts
  • app/lib/services/ddpSocket.test.ts
  • app/sagas/__tests__/state.test.ts
  • app/sagas/state.js
  • patches/@rocket.chat+sdk+1.3.3-mobile.patch
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: E2E Build iOS / ios-build
  • GitHub Check: E2E Build Android / android-build
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/sagas/__tests__/state.test.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/services/connect.test.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/sagas/state.js
  • app/lib/services/ddpSocket.test.ts
  • app/lib/services/connect.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript strict mode; resolve application imports relative to the app/ base URL.

Files:

  • app/sagas/__tests__/state.test.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/services/connect.test.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/lib/services/ddpSocket.test.ts
  • app/lib/services/connect.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Follow the repository Prettier style: tabs, single quotes, 130-character width, no trailing commas, omitted arrow-function parentheses where allowed, and same-line brackets.

Files:

  • app/sagas/__tests__/state.test.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/services/connect.test.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/sagas/state.js
  • app/lib/services/ddpSocket.test.ts
  • app/lib/services/connect.ts
app/sagas/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use sagas for side effects such as initialization, authentication, rooms, messages, encryption, deep linking, and video conferencing.

Files:

  • app/sagas/__tests__/state.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Jest tests with TZ=UTC to ensure deterministic timezone-dependent test behavior.

Files:

  • app/sagas/__tests__/state.test.ts
  • app/lib/services/connect.test.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/lib/services/ddpSocket.test.ts
app/lib/services/{sdk,restApi,connect}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use the SDK service for WebSocket subscriptions, the REST API service for HTTP requests via fetch, and the connect service for server connection management.

Files:

  • app/lib/services/connect.ts
🧠 Learnings (4)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/sagas/__tests__/state.test.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/services/connect.test.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/lib/services/ddpSocket.test.ts
  • app/lib/services/connect.ts
📚 Learning: 2026-06-25T18:37:25.526Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.test.tsx:16-22
Timestamp: 2026-06-25T18:37:25.526Z
Learning: In Rocket.Chat ReactNative tests that mock selectors for `useAppSelector`, don’t require the mocked selector input to be typed as `IApplicationState` when the fixture only includes a partial Redux state slice (e.g., only `server` and `settings`). Requiring the full `IApplicationState` type in that scenario forces unsafe `as IApplicationState` casts and undermines type-safety. For these narrowly scoped selector-mock fixtures, use a less strict type (e.g., `any`) to keep the mock focused on the slice under test.

Applied to files:

  • app/sagas/__tests__/state.test.ts
  • app/lib/services/connect.test.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/lib/services/ddpSocket.test.ts
📚 Learning: 2026-05-07T13:19:52.152Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7304
File: app/sagas/deepLinking.js:237-243
Timestamp: 2026-05-07T13:19:52.152Z
Learning: In this codebase’s Redux-Saga usage, remember that `yield put(action)` dispatches through the Redux store synchronously, and any saga(s) that synchronously react via action listeners (and synchronous `put` chains) will run to completion before the calling saga resumes at its next `yield`. As a result, within a single saga there is no scheduler interleaving between a `yield select(...)` and a subsequent `yield take(...)` at the next `yield` point, so a check-then-take pattern like `const state = yield select(...); if (state !== TARGET) { yield take(a => a.type === TARGET); }` is safe from TOCTOU races under the synchronous `put`/take model described above.

Applied to files:

  • app/sagas/state.js
📚 Learning: 2026-05-05T21:08:33.177Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7298
File: patches/@rocket.chat+sdk+1.3.3-mobile.patch:79-79
Timestamp: 2026-05-05T21:08:33.177Z
Learning: In the RocketChat/Rocket.Chat.ReactNative repo, for patches under patches/*.patch (especially those touching rocket.chat/sdk), remember that the patching sandbox's node_modules reflects the pre-patch state. Grepping node_modules for symbols (e.g., userDisconnectCloseCode = 4000 in node_modules/rocket.chat/sdk/lib/drivers/ddp.ts) can yield false positives. Review patches by inspecting the diff and applying it to a fresh copy of the SDK or diffing against the SDK source, rather than relying on node_modules. Ensure the patch actually introduces/updates symbols in the SDK source and run the test suite to validate behavior after applying the patch.

Applied to files:

  • patches/@rocket.chat+sdk+1.3.3-mobile.patch
🔇 Additional comments (15)
patches/@rocket.chat+sdk+1.3.3-mobile.patch (7)

2-11: LGTM!


19-41: LGTM!


46-58: LGTM!


96-150: forceReopen design looks sound.

Registry preservation, the _reopenInFlight guard, and the synchronous disconnected/close emission before teardown are all consistent with the documented Zombie-Connection fix and match the ddpSocket.test.ts assertions.


184-192: LGTM!


219-263: LGTM!


264-272: LGTM!

app/lib/services/ddpSocket.test.ts (1)

109-181: Solid coverage for registry preservation, replay, and dedup.

Tests correctly exercise forceReopen registry preservation, subscribeAll replay-with-original-id, and subscribe() dedup with callback fan-out, matching the patch's intended contract.

app/lib/services/connect.test.ts (2)

605-647: LGTM on test scenarios themselves (aside from the duplicate declaration flagged separately).


611-613: 🎯 Functional Correctness

No change needed — there’s no duplicate ResumeAction declaration.

The file contains a single type ResumeAction declaration inside the “connect — connected-guard” scope, so there’s no TS2300 duplicate-identifier issue here.

			> Likely an incorrect or invalid review comment.
app/sagas/__tests__/state.test.ts (1)

33-73: LGTM!

CONTEXT.md (1)

183-189: LGTM!

app/lib/methods/subscriptions/room.ts (1)

96-117: Teardown removal is justified by the SDK-level dedup contract.

Skipping unsubscribe() on the prior this.promises in handleLogin relies entirely on the SDK's registry preservation + (name, params) dedup (verified in ddpSocket.test.ts) to prevent server-side subscription leaks across repeated reopen cycles. Since sdk.subscribeRoom(this.rid) reuses the same (name, [rid]) key on every call, the SDK collapses repeats into the single preserved registry entry, so this is consistent with the cross-layer design.

app/lib/methods/subscriptions/room.test.ts (1)

196-236: LGTM!

app/lib/services/connect.ts (1)

119-131: 🩺 Stability & Availability

No change needed here. The login saga runs takeLatest(types.LOGIN.REQUEST, handleLoginRequest), so concurrent LOGIN.REQUEST actions coalesce to the latest request.

			> Likely an incorrect or invalid review comment.

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.75.0.109406

@github-actions

Copy link
Copy Markdown

…ent clock skew

subscription.lastOpen doubles as the chat.syncMessages cursor but was
written from the client clock and advanced unconditionally after every
room open. A client clock ahead of the server orphans messages that
arrived while the app was backgrounded: the server answers a future
cursor with a successful empty response, the app re-advances the
cursor, and the window is skipped forever.

The cursor now advances only from the max _updatedAt of persisted
batches (advanceSyncCursor), never on an empty fetch and never from
older-history windows. readMessages no longer writes lastOpen and
updateLastOpen (client-clock reseed on unsubscribe) is removed.

Also fixes two sync bugs in loadMissedMessages: DELETED pagination
results landing in the updated slot when only deletedNext paginated,
and the un-awaited recursive pagination call.
Advancing lastOpen per page was unsafe across the independent
UPDATED/DELETED streams: a newer timestamp from one stream's page could
move the shared cursor past records the other stream still had pending,
skipping them permanently if a later page failed. Pages are still
persisted as they arrive, but the cursor now advances exactly once after
the whole chain completes.

Also re-checks lastOpen inside the database write so concurrent syncs
can't move the cursor backward.

Addresses CodeRabbit review on #7506.
Stands up the real WatermelonDB JS core (writer lock, db.write
serialization, queries) over an in-memory LokiJSAdapter and the
production appSchema, mocking only sdk. Ships a shared fake >=7.1
chat.syncMessages server (paginated UPDATED/DELETED cursors, failure
injection) and seed/reset helpers for the sync-cursor integration
suites on PR #7506.
Add advanceSyncCursor.test.ts (9 tests) asserting the real persisted
subscription.lastOpen on the LokiJS in-memory WatermelonDB harness, with
only log mocked and the real getSubscriptionByRoomId running against the
live DB. Covers max-of-batch, forward-only (older + equal), empty batch,
all-missing _updatedAt, missing-skipped-without-poisoning, no-subscription
clean return, and error swallow+log.

The re-read-inside-write guard is exercised deterministically against the
real writer lock: a gated concurrent db.write holds the lock while
advanceSyncCursor reads the stale cursor and queues behind it, then commits
a higher timestamp so the in-flight write must skip its stale value rather
than regress. Mutating the guard to an unconditional set fails this test.

Also fix 3 pre-existing tsc errors in the harness (optional Relation access
and SubscriptionType cast) so the project tsc gate passes.
Cover the !args.latest gate in loadMessagesForRoom: newest load advances the
sync cursor to the window max; a scroll-up window (latest set) never advances
it; and the edited-old-message defense keeps the cursor pinned when scroll-up
surfaces an old message carrying a recent _updatedAt. Real persisted messages
+ real subscription.lastOpen on the LokiJS harness; only sdk/auxStore/
encryption/log mocked. Gate proven load-bearing by mutation.
The LokiJS integration harness left the in-memory driver open, so a full
`jest --runInBand` (the CI command) hung after a green run ("Jest did not
exit one second after the test run has completed"). Add closeLokiTestDatabase
— reaching the in-process driver the same way the adapter's own testClone does
(_dispatcher._worker._bridge.driver) — and call it from afterAll in every suite
that builds a database. The real CI command now exits on its own; no
--forceExit needed.

Also exclude the harness helpers (lokiTestDatabase.ts, fakeSyncServer.ts) from
jest collection via testPathIgnorePatterns, matching the existing
testHelpers.tsx entry. jest-expo's testMatch globs every __tests__/**/*, so
these non-test helpers failed the full suite with "Your test suite must
contain at least one test".
The re-read-guard test deliberately queues a db.write behind a running
writer. In non-production builds WatermelonDB's WorkQueue.enqueue arms a
~1.5s setTimeout to warn about a possibly stuck queue whenever a write is
enqueued behind an in-flight one. That live timer outlives Jest's 1s
post-run window and prints the benign "Jest did not exit one second after
the test run has completed" warning on the advanceSyncCursor suite.

Wrap the contended section in withWriterQueueDiagnosticCleared, which
captures and clears that timer, so the event loop drains and the suite
exits cleanly without --forceExit. No assertion or behavior changed.
babel-jest hoists jest.mock() to the top of the module regardless of
source position (the factories only close over mock-prefixed vars), so
the real imports never needed to sit below the mocks behind
// eslint-disable-next-line import/first. Move them into the normal top
import block and place the jest.mock() calls after, removing the disable
comments and the import/order 'empty line within import group' nit. No
behavior change; mocking still applies identically.
…B suites

loadMissedMessages.test.ts is entirely superseded — its 5 cases now have
mutation-proven real-DB equivalents in loadMissedMessages.integration.test.ts
(paginated deletions, no-advance-on-failure) and reconnectCycle.integration.test.ts
(offline coverage-gap, clock skew, backgrounded delivery). Delete the file.

subscriptions/room.test.ts keeps its subscribe/handleLogin/handleClose/DDP-recovery/
isAlive coverage but drops the lone updateMessage-concurrency case: the mock cannot
take the WMDB writer lock and only models a hand-rolled "pending changes" string, so
room.integration.test.ts supersedes it with the real prepareUpdate diagnostic.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/lib/database/__tests__/advanceSyncCursor.test.ts (1)

17-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated Loki-integration-test scaffolding into lokiTestDatabase.ts. All four new integration test files re-declare the identical mockActiveDatabase + jest.mock('../../database', ...) getter-factory, and three of them additionally duplicate the same T0..T5 time constants and persistedLastOpen helper verbatim. One shared root cause: no common test-utility module for this boilerplate, even though lokiTestDatabase.ts already hosts the sibling shared helpers (createLokiTestDatabase, seedSubscription, etc.).

  • app/lib/database/__tests__/advanceSyncCursor.test.ts#L17-L49: move the jest.mock('../../database', ...) factory pattern, T0..T5 constants, and persistedLastOpen helper into lokiTestDatabase.ts and import them here.
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts#L14-L74: replace the duplicated factory/constants/persistedLastOpen with the shared import.
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts#L20-L75: replace the duplicated factory/constants/persistedLastOpen with the shared import.
  • app/lib/database/__tests__/room.integration.test.ts#L22-L62: replace the duplicated jest.mock('../../database', ...) factory with the shared import (its T1/T2 constants are a smaller, differently-scoped variant of the same pattern and can be left local or folded in at the author's discretion).
🤖 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 `@app/lib/database/__tests__/advanceSyncCursor.test.ts` around lines 17 - 49,
Extract the repeated Loki test scaffolding into lokiTestDatabase.ts. In
app/lib/database/__tests__/advanceSyncCursor.test.ts#L17-L49, move the
mockActiveDatabase database mock, T0–T5 constants, and persistedLastOpen helper,
then import them; apply the same replacement in
app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts#L14-L74 and
app/lib/database/__tests__/loadMissedMessages.integration.test.ts#L20-L75. In
app/lib/database/__tests__/room.integration.test.ts#L22-L62, replace only the
duplicated database mock with the shared utility, leaving its local T1/T2
constants or folding them into the shared constants as appropriate.
app/lib/database/__tests__/lokiTestDatabase.ts (1)

74-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Guard closeLokiTestDatabase against adapter layout changes.

The helper currently relies on private WatermelonDB internals (_dispatcher._worker._bridge.driver.loki) that are not exposed by the LokiJS adapter API, so an upgrade can silently break cleanup. Keep this code behind a targeted compatibility fallback or a documented internal-api boundary if this cleanup path must remain.

🤖 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 `@app/lib/database/__tests__/lokiTestDatabase.ts` around lines 74 - 84, Update
closeLokiTestDatabase to isolate the private WatermelonDB adapter traversal
behind a targeted, documented compatibility fallback, and preserve the no-op
behavior when Loki’s close method is unavailable. Avoid depending directly on
the internal _dispatcher/_worker/_bridge/driver.loki layout without explicitly
marking or containing that internal-API boundary.
🤖 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 `@app/lib/methods/loadMissedMessages.ts`:
- Around line 88-125: Replace full message accumulation in syncPages with only
the _updatedAt values needed by advanceSyncCursor, and update
advanceSyncCursor’s parameter type to accept this lightweight shape. Preserve
timestamp collection for both updated and deleted records and pass the
lightweight accumulator through loadMissedMessages without retaining complete
payloads.

---

Nitpick comments:
In `@app/lib/database/__tests__/advanceSyncCursor.test.ts`:
- Around line 17-49: Extract the repeated Loki test scaffolding into
lokiTestDatabase.ts. In
app/lib/database/__tests__/advanceSyncCursor.test.ts#L17-L49, move the
mockActiveDatabase database mock, T0–T5 constants, and persistedLastOpen helper,
then import them; apply the same replacement in
app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts#L14-L74 and
app/lib/database/__tests__/loadMissedMessages.integration.test.ts#L20-L75. In
app/lib/database/__tests__/room.integration.test.ts#L22-L62, replace only the
duplicated database mock with the shared utility, leaving its local T1/T2
constants or folding them into the shared constants as appropriate.

In `@app/lib/database/__tests__/lokiTestDatabase.ts`:
- Around line 74-84: Update closeLokiTestDatabase to isolate the private
WatermelonDB adapter traversal behind a targeted, documented compatibility
fallback, and preserve the no-op behavior when Loki’s close method is
unavailable. Avoid depending directly on the internal
_dispatcher/_worker/_bridge/driver.loki layout without explicitly marking or
containing that internal-API boundary.
🪄 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 Plus

Run ID: edbf7a4b-8187-4df5-8e7a-ca15f2f660ce

📥 Commits

Reviewing files that changed from the base of the PR and between 849e846 and e386cc1.

📒 Files selected for processing (17)
  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/lib/methods/readMessages.ts
  • app/lib/methods/subscriptions/room.test.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/methods/updateLastOpen.ts
  • app/views/RoomView/index.tsx
  • jest.config.js
💤 Files with no reviewable changes (3)
  • app/lib/methods/updateLastOpen.ts
  • app/lib/methods/subscriptions/room.ts
  • app/lib/methods/subscriptions/room.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/readMessages.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
  • jest.config.js
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript strict mode; resolve application imports relative to the app/ base URL.

Files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/readMessages.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Follow the repository Prettier style: tabs, single quotes, 130-character width, no trailing commas, omitted arrow-function parentheses where allowed, and same-line brackets.

Files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/readMessages.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
  • jest.config.js
app/lib/database/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use WatermelonDB for the offline-first SQLite database; keep models under model/ and schema definitions under schema/.

Files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Jest tests with TZ=UTC to ensure deterministic timezone-dependent test behavior.

Files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
🧠 Learnings (4)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/readMessages.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
📚 Learning: 2026-06-25T18:37:25.526Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.test.tsx:16-22
Timestamp: 2026-06-25T18:37:25.526Z
Learning: In Rocket.Chat ReactNative tests that mock selectors for `useAppSelector`, don’t require the mocked selector input to be typed as `IApplicationState` when the fixture only includes a partial Redux state slice (e.g., only `server` and `settings`). Requiring the full `IApplicationState` type in that scenario forces unsafe `as IApplicationState` casts and undermines type-safety. For these narrowly scoped selector-mock fixtures, use a less strict type (e.g., `any`) to keep the mock focused on the slice under test.

Applied to files:

  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/lib/database/__tests__/room.integration.test.ts
  • app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts
  • app/lib/database/__tests__/reconnectCycle.integration.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
📚 Learning: 2026-06-24T22:58:43.390Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7157
File: app/views/MessagesView/index.tsx:392-392
Timestamp: 2026-06-24T22:58:43.390Z
Learning: When wrapping a React Native component (e.g., via `withSafeAreaInsets`) ensure `hoistNonReactStatics` is only required if the wrapped component actually defines static properties/methods that consumers rely on. If the component has no statics (as in `app/views/MessagesView/index.tsx`), you can omit `hoistNonReactStatics` for this case.

Applied to files:

  • app/views/RoomView/index.tsx
📚 Learning: 2026-06-25T18:37:44.793Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.tsx:101-141
Timestamp: 2026-06-25T18:37:44.793Z
Learning: In the Rocket.Chat React Native codebase, do not treat passing an `async` function directly to an event prop in React/React Native UI components (e.g., `onPress={async () => ...}` in TSX) as a “floating promises” CI-blocking lint issue—this repo does not enable the ESLint `no-floating-promises` rule (while `no-void` is enforced). Only raise robustness follow-ups when there are genuinely unhandled promise paths (e.g., fire-and-forget calls like `save()` that return a Promise that is neither awaited nor handled), and prefer making sure failure paths are explicitly handled/reported rather than blocking on lint-style floating-promise concerns.

Applied to files:

  • app/views/RoomView/index.tsx
🔇 Additional comments (14)
app/views/RoomView/index.tsx (1)

681-695: LGTM!

app/lib/methods/readMessages.ts (1)

8-8: LGTM!

Also applies to: 47-48

app/lib/methods/helpers/advanceSyncCursor.ts (1)

1-51: LGTM!

app/lib/methods/loadMessagesForRoom.ts (1)

8-8: LGTM!

Also applies to: 142-146

app/lib/methods/loadMissedMessages.ts (1)

1-3: LGTM!

Also applies to: 23-37

app/lib/database/__tests__/reconnectCycle.integration.test.ts (1)

1-178: LGTM!

jest.config.js (1)

3-10: LGTM!

app/lib/database/__tests__/advanceSyncCursor.test.ts (1)

63-169: LGTM!

app/lib/database/__tests__/loadMessagesForRoom.integration.test.ts (1)

88-132: LGTM!

app/lib/database/__tests__/loadMissedMessages.integration.test.ts (1)

92-209: LGTM!

app/lib/database/__tests__/lokiTestDatabase.test.ts (1)

1-70: LGTM!

app/lib/database/__tests__/fakeSyncServer.ts (1)

58-117: LGTM!

app/lib/database/__tests__/room.integration.test.ts (2)

139-153: 🎯 Functional Correctness | ⚡ Quick win

Verify database.batch() call shape matches this test's variadic assumption.

batchSpy.mock.calls[0] having length 3 only holds if RoomSubscription.updateMessage calls database.batch(record1, record2, record3) (spread). WatermelonDB's batch() also accepts a single array argument (database.batch([...])), which would make this call have length 1 instead and fail the assertion. Since room.ts isn't in this review batch, please confirm the implementation's actual call shape.


106-137: LGTM!

Also applies to: 155-178

Comment thread app/lib/methods/loadMissedMessages.ts Outdated
@diegolmello
diegolmello changed the base branch from develop to swamp-string July 24, 2026 18:52
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat 4.75.0.109408

@diegolmello
diegolmello changed the base branch from swamp-string to develop July 25, 2026 13:56
A subscription whose lastOpen is unset — typically a room reached by
tapping a push notification — resolved an undefined chat.syncMessages
cursor, so loadMissedMessages issued no requests and resolved as
success. Nothing was ever backfilled.

getLastUpdate now resolves the cursor from the first candidate that is a
real point in time: lastOpen, then the newest server-stamped message
this device holds, then ls, then ts. ls is written by every device, so
it can sit past messages this one never received; the newest persisted
message reconstructs what advanceSyncCursor would have stored. Rows
stamped from the device clock — synthetic load-more sentinels and unsent
TEMP/ERROR sends — are excluded, since taking one as the cursor would
ask the server for changes newer than a clock that may run ahead.

loadMessagesForRoom keeps the synthetic load-more row out of the array
handed to advanceSyncCursor for the same reason: normalizeMessage stamps
its missing _updatedAt with new Date().

Covered by LokiJS integration suites asserting the recovered message
row, not just the cursor value.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts (1)

49-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: prefer an interface over a type alias for this object shape.

As per coding guidelines, **/*.{ts,tsx} should "Prefer interfaces over type aliases for defining object shapes in TypeScript." TSdkGetRouter describes an object shape and could be an interface instead.

🤖 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 `@app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts` around
lines 49 - 52, Change TSdkGetRouter from a type alias to an interface while
preserving its mockImplementation method signature and parameter types.

Source: Coding guidelines

app/lib/database/services/Message.ts (1)

32-45: 🚀 Performance & Scalability | 🔵 Trivial

Verify a composite index backs this rid + _updated_at DESC + take query.

This query now runs on the reconnect/resume hot path (via loadMissedMessagesgetLastUpdate) for potentially many rooms. WatermelonDB supports Q.sortBy/Q.take on the SQLite adapter, but doesn't auto-create composite indexes — those need to be declared in the schema. Without an index covering (rid, _updated_at), SQLite will scan and sort all of a room's messages before limiting to NEWEST_MESSAGE_SCAN.

Please confirm the messages schema (not included in this review) has an appropriate index.

🤖 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 `@app/lib/database/services/Message.ts` around lines 32 - 45, Ensure the
messages schema declares a composite index covering rid and _updated_at, with
ordering appropriate for the getNewestMessageUpdatedAt query’s Q.where, Q.sortBy
descending, and Q.take usage. Update the schema definition rather than changing
getNewestMessageUpdatedAt, and preserve the existing query behavior.
🤖 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 `@app/lib/methods/loadMissedMessages.ts`:
- Around line 45-53: Update getLastUpdate so it validates sub.lastOpen first and
returns it immediately when it is a valid positive-date cursor; only call
getNewestMessageUpdatedAt(rid) when lastOpen is absent or invalid, then preserve
the existing fallback evaluation using sub.ls and sub.ts.

---

Nitpick comments:
In `@app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts`:
- Around line 49-52: Change TSdkGetRouter from a type alias to an interface
while preserving its mockImplementation method signature and parameter types.

In `@app/lib/database/services/Message.ts`:
- Around line 32-45: Ensure the messages schema declares a composite index
covering rid and _updated_at, with ordering appropriate for the
getNewestMessageUpdatedAt query’s Q.where, Q.sortBy descending, and Q.take
usage. Update the schema definition rather than changing
getNewestMessageUpdatedAt, and preserve the existing query behavior.
🪄 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 Plus

Run ID: 4fbde26b-4968-4032-8f36-00977c084495

📥 Commits

Reviewing files that changed from the base of the PR and between e386cc1 and 94d3945.

📒 Files selected for processing (8)
  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
  • app/lib/database/services/Message.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/methods/loadMissedMessages.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/lib/database/tests/advanceSyncCursor.test.ts
  • app/lib/database/tests/lokiTestDatabase.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/database/services/Message.ts
  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript strict mode; resolve application imports relative to the app/ base URL.

Files:

  • app/lib/database/services/Message.ts
  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Follow the repository Prettier style: tabs, single quotes, 130-character width, no trailing commas, omitted arrow-function parentheses where allowed, and same-line brackets.

Files:

  • app/lib/database/services/Message.ts
  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
app/lib/database/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use WatermelonDB for the offline-first SQLite database; keep models under model/ and schema definitions under schema/.

Files:

  • app/lib/database/services/Message.ts
  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Jest tests with TZ=UTC to ensure deterministic timezone-dependent test behavior.

Files:

  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
🧠 Learnings (2)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/database/services/Message.ts
  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
  • app/lib/methods/loadMissedMessages.ts
📚 Learning: 2026-06-25T18:37:25.526Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.test.tsx:16-22
Timestamp: 2026-06-25T18:37:25.526Z
Learning: In Rocket.Chat ReactNative tests that mock selectors for `useAppSelector`, don’t require the mocked selector input to be typed as `IApplicationState` when the fixture only includes a partial Redux state slice (e.g., only `server` and `settings`). Requiring the full `IApplicationState` type in that scenario forces unsafe `as IApplicationState` casts and undermines type-safety. For these narrowly scoped selector-mock fixtures, use a less strict type (e.g., `any`) to keep the mock focused on the slice under test.

Applied to files:

  • app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
🔇 Additional comments (3)
app/lib/methods/loadMessagesForRoom.ts (1)

8-8: LGTM!

Also applies to: 132-150

app/lib/database/__tests__/cursorClockPoisoning.integration.test.ts (1)

1-48: LGTM!

Also applies to: 53-190

app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts (1)

1-195: LGTM!

Comment on lines +45 to 53
const getLastUpdate = async (rid: string): Promise<Date | null> => {
const sub = await getSubscriptionByRoomId(rid);
if (!sub) {
return null;
}
return sub.lastOpen;
const candidates = [sub.lastOpen, await getNewestMessageUpdatedAt(rid), sub.ls, sub.ts];
const cursor = candidates.find(candidate => candidate && new Date(candidate).getTime() > 0);
return cursor ? new Date(cursor) : null;
};

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Unconditional getNewestMessageUpdatedAt query even when lastOpen is already valid.

The candidates array literal evaluates await getNewestMessageUpdatedAt(rid) unconditionally before .find() runs, so this extra WatermelonDB scan/sort executes even when sub.lastOpen is already a valid cursor — the common case for any room that's been opened before. Since this runs per-room during the resume/resubscribe flow, it's an avoidable DB round-trip on a hot path.

⚡ Proposed fix: short-circuit before the async fallback
 const getLastUpdate = async (rid: string): Promise<Date | null> => {
 	const sub = await getSubscriptionByRoomId(rid);
 	if (!sub) {
 		return null;
 	}
-	const candidates = [sub.lastOpen, await getNewestMessageUpdatedAt(rid), sub.ls, sub.ts];
-	const cursor = candidates.find(candidate => candidate && new Date(candidate).getTime() > 0);
-	return cursor ? new Date(cursor) : null;
+	const isValidCursor = (candidate?: Date | string | number | null) => !!candidate && new Date(candidate).getTime() > 0;
+	if (isValidCursor(sub.lastOpen)) {
+		return new Date(sub.lastOpen as Date);
+	}
+	const cursor = [await getNewestMessageUpdatedAt(rid), sub.ls, sub.ts].find(isValidCursor);
+	return cursor ? new Date(cursor) : null;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getLastUpdate = async (rid: string): Promise<Date | null> => {
const sub = await getSubscriptionByRoomId(rid);
if (!sub) {
return null;
}
return sub.lastOpen;
const candidates = [sub.lastOpen, await getNewestMessageUpdatedAt(rid), sub.ls, sub.ts];
const cursor = candidates.find(candidate => candidate && new Date(candidate).getTime() > 0);
return cursor ? new Date(cursor) : null;
};
const getLastUpdate = async (rid: string): Promise<Date | null> => {
const sub = await getSubscriptionByRoomId(rid);
if (!sub) {
return null;
}
const isValidCursor = (candidate?: Date | string | number | null) =>
!!candidate && new Date(candidate).getTime() > 0;
if (isValidCursor(sub.lastOpen)) {
return new Date(sub.lastOpen as Date);
}
const cursor = [await getNewestMessageUpdatedAt(rid), sub.ls, sub.ts].find(isValidCursor);
return cursor ? new Date(cursor) : null;
};
🤖 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 `@app/lib/methods/loadMissedMessages.ts` around lines 45 - 53, Update
getLastUpdate so it validates sub.lastOpen first and returns it immediately when
it is a valid positive-date cursor; only call getNewestMessageUpdatedAt(rid)
when lastOpen is absent or invalid, then preserve the existing fallback
evaluation using sub.ls and sub.ts.

The subscription idempotency key used a literal NUL byte as its separator,
which put a raw 0x00 into patches/@rocket.chat+sdk+1.3.3-mobile.patch. Git
classified the file as binary, so the ~130 SDK lines that are the substance of
this change - the dedup guard, the _pendingSubscriptions claim, the
subscribeAll replay - rendered as "Bin 7546 -> 12177 bytes" and could not be
read in any diff view.

Write the separator as a unicode escape instead. The runtime key is the same
character, so subscription identity is unchanged.

Also mark *.patch as text with an explicit diff attribute, so a future control
byte in a patch cannot hide its contents again.
An unbounded awaited page walk in `syncPages` met an uncapped 300ms init retry in
RoomView, which together could hold ~13-70 requests/second indefinitely.

- bound the `chat.syncMessages` chain with MAX_PAGES, mirroring
  loadMessagesForRoom's MAX_BATCHES; the next sync resumes from the persisted cursor
- thread a running max `_updatedAt` through `syncPages` instead of accumulating
  every page's full message payload for the whole chain
- back the RoomView init retry off exponentially (300ms doubling) and give up after
  five attempts; the budget is restored once an init succeeds
- feed only `updated` to `advanceSyncCursor` and retype `deleted` as
  `{ _id, _deletedAt }[]`, matching what the server projection actually carries, so
  the cursor's independence from deletion times is structural, not incidental
The page bound made the persisted cursor wrong whenever truncation hit the DELETED
stream: only the UPDATED max was carried out, so a truncated DELETED walk either
livelocked (deletions with no updates left the cursor unmoved and every later sync
re-walked the same ten pages) or skipped deletions between the abandoned stop-point
and a further-along updated max.

- `syncPages` now returns `{ highestUpdatedAt, deletedStop }` and the cursor resolves
  to the lower of the two live stop-points; both streams filter exclusively, so that
  costs a redundant refetch, never a record
- the fake sync server models deleted records the way the server projects them —
  `{ _id, _deletedAt }`, paginated and sorted on `_deletedAt`
- the RoomView init retry keeps the exponential backoff but drops the attempt cap and
  settles at the 10s floor: nothing else re-enters init() for an authenticated room, so
  giving up left the room blank with no retry and no error affordance
- `syncPages` takes an options object, `advanceSyncCursor`'s parameter says
  `serverLatest`, and the stale @ts-ignore reason states the real mismatch
A room that keeps failing now retries forever at the 10s floor, so the failure left no
record at all. Record the first failure since the last successful init and stay silent
for the rest of the loop.

Also: `advanceSyncCursor`'s contract note admits a `_deletedAt` stop-point, the
exhausted-stream check reads `deletedStop === null`, and the fake sync server states
that its DELETED cursor is the intended contract rather than the shipped server's.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/lib/database/__tests__/fakeSyncServer.ts (1)

91-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add explicit signatures to the local helpers.

paginate, handleSyncMessages, and installOn rely on inferred/contextual return and parameter types. Declare them explicitly to satisfy the TypeScript guideline.

As per coding guidelines, “add explicit type annotations to function parameters and return types.”

Also applies to: 114-130, 132-139

🤖 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 `@app/lib/database/__tests__/fakeSyncServer.ts` around lines 91 - 96, Update
the local helpers paginate, handleSyncMessages, and installOn with explicit type
annotations for every parameter and their return types, replacing reliance on
inference or contextual typing. Preserve their existing behavior and use the
surrounding types and return shapes already established in fakeSyncServer.

Source: Coding guidelines

🤖 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 `@app/lib/database/__tests__/fakeSyncServer.ts`:
- Around line 12-15: Update the DELETED behavior in the fake sync server and its
associated documentation so it does not present `_deletedAt`-based cursors as
the supported production contract. Either reproduce mountCursorQuery’s deployed
ordering after projection or explicitly mark the fake as intentionally
divergent, and ensure tests avoid asserting unsupported DELETED page ordering or
cursor semantics.

In `@app/views/RoomView/index.tsx`:
- Line 383: Update the RoomView initialization retry flow around init() and the
cleanup containing this.initRetry.cancel() so async success and catch paths
first verify this.mounted (or an equivalent lifecycle generation token) before
updating state or scheduling retries. Ensure an in-flight init rejection after
unmount cannot retain RoomView or trigger further network work, while preserving
normal mounted retry behavior.

---

Nitpick comments:
In `@app/lib/database/__tests__/fakeSyncServer.ts`:
- Around line 91-96: Update the local helpers paginate, handleSyncMessages, and
installOn with explicit type annotations for every parameter and their return
types, replacing reliance on inference or contextual typing. Preserve their
existing behavior and use the surrounding types and return shapes already
established in fakeSyncServer.
🪄 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 Plus

Run ID: 60fb9d5e-96c8-4584-9510-21b835a2af55

📥 Commits

Reviewing files that changed from the base of the PR and between 42d1672 and 2b11182.

📒 Files selected for processing (11)
  • app/lib/database/__tests__/advanceSyncCursor.test.ts
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
  • app/lib/database/__tests__/lokiTestDatabase.test.ts
  • app/lib/database/__tests__/nullCursorRecurrence.integration.test.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/views/RoomView/index.tsx
  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/views/RoomView/services/initRetryScheduler.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/lib/methods/loadMessagesForRoom.ts
  • app/lib/database/tests/lokiTestDatabase.test.ts
  • app/lib/database/tests/advanceSyncCursor.test.ts
  • app/lib/methods/loadMissedMessages.ts
  • app/lib/database/tests/nullCursorRecurrence.integration.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/views/RoomView/services/initRetryScheduler.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

Use TypeScript strict mode; resolve application imports relative to the app/ base URL.

Files:

  • app/views/RoomView/services/initRetryScheduler.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Follow the repository Prettier style: tabs, single quotes, 130-character width, no trailing commas, omitted arrow-function parentheses where allowed, and same-line brackets.

Files:

  • app/views/RoomView/services/initRetryScheduler.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run Jest tests with TZ=UTC to ensure deterministic timezone-dependent test behavior.

Files:

  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
app/lib/database/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use WatermelonDB for the offline-first SQLite database; keep models under model/ and schema definitions under schema/.

Files:

  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
🧠 Learnings (4)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/views/RoomView/services/initRetryScheduler.ts
  • app/lib/methods/helpers/advanceSyncCursor.ts
  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/views/RoomView/index.tsx
  • app/lib/database/__tests__/fakeSyncServer.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
📚 Learning: 2026-06-25T18:37:25.526Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.test.tsx:16-22
Timestamp: 2026-06-25T18:37:25.526Z
Learning: In Rocket.Chat ReactNative tests that mock selectors for `useAppSelector`, don’t require the mocked selector input to be typed as `IApplicationState` when the fixture only includes a partial Redux state slice (e.g., only `server` and `settings`). Requiring the full `IApplicationState` type in that scenario forces unsafe `as IApplicationState` casts and undermines type-safety. For these narrowly scoped selector-mock fixtures, use a less strict type (e.g., `any`) to keep the mock focused on the slice under test.

Applied to files:

  • app/views/RoomView/services/initRetryScheduler.test.ts
  • app/lib/database/__tests__/loadMissedMessages.integration.test.ts
📚 Learning: 2026-06-24T22:58:43.390Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7157
File: app/views/MessagesView/index.tsx:392-392
Timestamp: 2026-06-24T22:58:43.390Z
Learning: When wrapping a React Native component (e.g., via `withSafeAreaInsets`) ensure `hoistNonReactStatics` is only required if the wrapped component actually defines static properties/methods that consumers rely on. If the component has no statics (as in `app/views/MessagesView/index.tsx`), you can omit `hoistNonReactStatics` for this case.

Applied to files:

  • app/views/RoomView/index.tsx
📚 Learning: 2026-06-25T18:37:44.793Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7434
File: app/views/ScreenLockConfigView.tsx:101-141
Timestamp: 2026-06-25T18:37:44.793Z
Learning: In the Rocket.Chat React Native codebase, do not treat passing an `async` function directly to an event prop in React/React Native UI components (e.g., `onPress={async () => ...}` in TSX) as a “floating promises” CI-blocking lint issue—this repo does not enable the ESLint `no-floating-promises` rule (while `no-void` is enforced). Only raise robustness follow-ups when there are genuinely unhandled promise paths (e.g., fire-and-forget calls like `save()` that return a Promise that is neither awaited nor handled), and prefer making sure failure paths are explicitly handled/reported rather than blocking on lint-style floating-promise concerns.

Applied to files:

  • app/views/RoomView/index.tsx
🔇 Additional comments (9)
app/views/RoomView/index.tsx (2)

118-118: LGTM!

Also applies to: 138-138, 680-680


694-694: 🗄️ Data Integrity & Integration

Verify the dropped readMessages argument against its new API.

This changes the call from (room.rid, readAt, true) to (room.rid, readAt). Confirm the new default preserves the intended read/cursor behavior and that all other callers use the updated contract.

app/views/RoomView/services/initRetryScheduler.test.ts (2)

1-94: LGTM!


1-94: 📐 Maintainability & Code Quality

Run this Jest file with TZ=UTC.

This file matches **/*.test.ts; execute it under UTC as required by the repository guidelines.

Source: Coding guidelines

app/views/RoomView/services/initRetryScheduler.ts (1)

1-36: LGTM!

app/lib/methods/helpers/advanceSyncCursor.ts (1)

6-18: LGTM!

Also applies to: 20-53

app/lib/database/__tests__/loadMissedMessages.integration.test.ts (2)

97-234: 📐 Maintainability & Code Quality

Run this integration suite with TZ=UTC.

The repository test guideline requires deterministic timezone execution for *.test.ts files.

As per coding guidelines, “Run Jest tests with TZ=UTC to ensure deterministic timezone-dependent test behavior.”

Source: Coding guidelines


196-198: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the second map.

Line 197 destructures the params object emitted by the first map as [, params], causing a runtime “not iterable” failure before the cursor assertion.

Proposed fix
 		const deletedCursors = mockedSdkGet.mock.calls
 			.map(([, params]) => params as unknown as { type?: string; next?: number })
-			.map(([, params]) => params as unknown as { type?: string; next?: number })
 			.filter(params => params.type === 'DELETED')
 			.map(params => params.next);
			> Likely an incorrect or invalid review comment.
app/lib/database/__tests__/fakeSyncServer.ts (1)

37-40: 🎯 Functional Correctness

Manual verification needed for the duplicate exported type aliases.

The repository contents could not be inspected in this environment to determine whether TFakeServerMessageWire or TFakeServerDeletedWire are declared multiple times in app/lib/database/__tests__/fakeSyncServer.ts.

Comment on lines +12 to +15
* This models the DELETED cursor as intended, not as shipped: the server's
* `mountCursorQuery` sorts unconditionally by `_updatedAt`, which the DELETED projection
* strips, so its real `next` is not guaranteed to be the page's max `_deletedAt`. Do not
* read this fake as ground truth for that stream's ordering.

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'fakeSyncServer\.ts$|test.*Sync|.*Sync.*test.*|.*sync.*' app | sed 's#^\./##' | head -100

echo
echo "fakeSyncServer outline:"
if [ -f app/lib/database/__tests__/fakeSyncServer.ts ]; then
  wc -l app/lib/database/__tests__/fakeSyncServer.ts
  ast-grep outline app/lib/database/__tests__/fakeSyncServer.ts || true
  echo
  echo "fakeSyncServer relevant lines 1-180:"
  cat -n app/lib/database/__tests__/fakeSyncServer.ts | sed -n '1,180p'
fi

echo
echo "Search for mountCursorQuery / DELETED / _deletedAt / _updatedAt in tests/helpers:"
rg -n "mountCursorQuery|DELETED|_deletedAt|_updatedAt|nextCursor|cursor" app/lib/database/__tests__ app -g '*.{ts,tsx,js,jsx}' | head -250

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2071


Use the supported DELETED cursor contract in the fake.

The fake emits DELETED next cursors from _deletedAt, but the deployed mountCursorQuery sorts DELETED entries by _updatedAt after the projection strips that field. Make this fake either mirror the production ordering behavior or mark it intentionally divergent so tests don’t rely on unsupported page ordering/cursor semantics.

🤖 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 `@app/lib/database/__tests__/fakeSyncServer.ts` around lines 12 - 15, Update
the DELETED behavior in the fake sync server and its associated documentation so
it does not present `_deletedAt`-based cursors as the supported production
contract. Either reproduce mountCursorQuery’s deployed ordering after projection
or explicitly mark the fake as intentionally divergent, and ensure tests avoid
asserting unsupported DELETED page ordering or cursor semantics.

if (this.retryInitTimeout) {
clearTimeout(this.retryInitTimeout);
}
this.initRetry.cancel();

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent retries from surviving unmount.

cancel() only removes a timer that has not fired. If an in-flight init() rejects after unmount, this catch still updates state and schedules another retry, retaining the dead RoomView and continuing network work. Guard the post-await success path and catch with this.mounted or a lifecycle generation token.

Suggested guard
 const member = await this.getRoomMember();
+if (!this.mounted) {
+	return;
+}
 this.setState({ canAutoTranslate, member, loading: false });
 this.initRetry.reset();
 } catch (e) {
+if (!this.mounted) {
+	return;
+}
 this.setState({ loading: false });

Also applies to: 702-709

🤖 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 `@app/views/RoomView/index.tsx` at line 383, Update the RoomView initialization
retry flow around init() and the cleanup containing this.initRetry.cancel() so
async success and catch paths first verify this.mounted (or an equivalent
lifecycle generation token) before updating state or scheduling retries. Ensure
an in-flight init rejection after unmount cannot retain RoomView or trigger
further network work, while preserving normal mounted retry behavior.

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