Skip to content

fix: enhance invite handling for re-join scenarios and prevent duplicate events#391

Open
sampaiodiego wants to merge 2 commits intomainfrom
invite-after-leave
Open

fix: enhance invite handling for re-join scenarios and prevent duplicate events#391
sampaiodiego wants to merge 2 commits intomainfrom
invite-after-leave

Conversation

@sampaiodiego
Copy link
Copy Markdown
Member

@sampaiodiego sampaiodiego commented Mar 27, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate notifications for already-known room events to reduce redundant alerts.
    • Improved invite handling when required room state or referenced prior events are missing by treating such invites as outliers.
    • Ensured re-join flows consistently refresh and apply initial room state for existing rooms.
  • Tests

    • Added comprehensive integration tests covering invites, re-joins, outlier persistence, and invite→join→leave→reinvite cycles.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fed317d6-c821-4ba9-933f-7f12f37f61b5

📥 Commits

Reviewing files that changed from the base of the PR and between 40038d2 and 2658e25.

📒 Files selected for processing (4)
  • packages/federation-sdk/src/services/invite.service.spec.ts
  • packages/federation-sdk/src/services/invite.service.ts
  • packages/federation-sdk/src/services/room.service.ts
  • packages/federation-sdk/src/services/state.service.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/federation-sdk/src/services/invite.service.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/federation-sdk/src/services/state.service.ts
📜 Recent review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-10T22:18:31.655Z
Learnt from: sampaiodiego
Repo: RocketChat/homeserver PR: 224
File: packages/federation-sdk/src/services/event-authorization.service.ts:261-268
Timestamp: 2025-10-10T22:18:31.655Z
Learning: In packages/federation-sdk/src/services/state.service.ts, the method `StateService.getLatestRoomState(roomId: string)` has return type `Promise<State>` and never returns undefined. If the state is not found, it throws an error with message "No state found for room ${roomId}" instead of returning undefined.

Applied to files:

  • packages/federation-sdk/src/services/invite.service.ts
  • packages/federation-sdk/src/services/room.service.ts
🔇 Additional comments (3)
packages/federation-sdk/src/services/invite.service.ts (2)

245-253: Existing concern about outlier events in canResolveEventState.

The past review comment correctly identifies that findByIds() returns outlier events (which have stateId: ''), potentially causing handlePdu() to fail even when this method returns true. The suggestion to also check auth events and filter out outliers remains relevant.


219-227: LGTM on the conditional gate logic.

The approach to only call handlePdu when both conditions are met (room create exists AND prev_events are resolvable), with fallback to insertOutlierEvent, is sound for handling re-invite scenarios after a user has left.

packages/federation-sdk/src/services/room.service.ts (1)

734-751: LGTM on the unconditional processInitialState call for re-join handling.

The refactor correctly ensures processInitialState is always called after send_join, which handles both new joins and re-joins. The idempotency is properly handled in processInitialState via the knownEventIds set (context snippet 1, lines 388-396 and 463-465 of state.service.ts), which prevents duplicate event notifications for events already in the database.

The error handling is also correct: only UnknownRoomError is caught for logging purposes, while other errors (e.g., database connection issues) are properly rethrown.


Walkthrough

Conditional invite handling now requires a local room-create and resolvable prev_events to process invites; otherwise invites are stored as outliers. State initialization preloads known event IDs to avoid re-notifying persisted events. Join flow now always calls processInitialState for re-joins.

Changes

Cohort / File(s) Summary
Invite logic & tests
packages/federation-sdk/src/services/invite.service.ts, packages/federation-sdk/src/services/invite.service.spec.ts
processInvite only calls stateService.handlePdu when a m.room.create exists and prev_events are resolvable; otherwise stores invite as outlier. Added canResolveEventState helper and an integration-style Mongo-backed test suite covering invite/rejoin/outlier behaviors.
State initialization
packages/federation-sdk/src/services/state.service.ts
processInitialState preloads persisted event IDs and skips eventService.notify for events already present while still persisting deltas and updating the room graph.
Join flow
packages/federation-sdk/src/services/room.service.ts
RoomService.joinUser now invokes processInitialState unconditionally after attempting to get room version (re-join path updated); non-UnknownRoom errors are rethrown.

Sequence Diagram(s)

sequenceDiagram
    participant RoomService as RoomService
    participant InviteService as InviteService
    participant EventRepository as EventRepository
    participant StateService as StateService
    participant Notification as NotificationService

    RoomService->>StateService: getRoomVersion(roomId)
    StateService-->>RoomService: room version / UnknownRoomError
    RoomService->>StateService: processInitialState(state, authChain)

    StateService->>EventRepository: preload event IDs for authChain/eventCache
    EventRepository-->>StateService: knownEventIds

    loop per sorted event
        alt eventId not in knownEventIds
            StateService->>Notification: notify(event)
            Notification-->>StateService: ack
        else
            StateService-->>StateService: skip notify
        end
        StateService->>StateService: persist state delta / update room graph
    end

    Note over InviteService,EventRepository: Invite handling flow
    InviteService->>EventRepository: check room-create exists
    InviteService->>EventRepository: canResolveEventState(prev_events)
    alt room-create exists AND prev_events resolvable
        InviteService->>StateService: handlePdu(inviteEvent)
        StateService-->>InviteService: processed
    else
        InviteService->>EventRepository: insertOutlierEvent(inviteEvent)
        EventRepository-->>InviteService: stored as outlier
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: improving invite handling for re-join scenarios and preventing duplicate event notifications, which are the core objectives evident from the code changes.

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


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

❤️ Share

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

@sampaiodiego sampaiodiego changed the title feat: enhance invite handling for re-join scenarios and prevent duplicate events fix: enhance invite handling for re-join scenarios and prevent duplicate events Mar 27, 2026
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/federation-sdk/src/services/state.service.ts">

<violation number="1" location="packages/federation-sdk/src/services/state.service.ts:424">
P2: The dedupe query runs after saving the create event, so `m.room.create` is incorrectly treated as pre-existing and never notified in this flow.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/federation-sdk/src/services/invite.service.spec.ts (1)

67-69: Hoist the EventService stub to a variable for cleaner test setup.

The spec files are excluded from TypeScript compilation (tsconfig.json excludes **/*.spec.ts), so the private member access here doesn't cause type errors. However, extracting the stub to a separate variable improves readability and test structure by clearly separating the mock setup from the service instantiation.

Cleaner test setup
-	const stateService = new StateService(stateGraphRepository, eventRepository, configServiceInstance, {
+	const eventService = {
 		notify: () => Promise.resolve(),
-	} as unknown as EventService);
+	} as unknown as EventService;
+
+	const stateService = new StateService(stateGraphRepository, eventRepository, configServiceInstance, eventService);
...
-			const notifySpy = spyOn(stateService.eventService as any, 'notify').mockImplementation(
+			const notifySpy = spyOn(eventService as any, 'notify').mockImplementation(

Also applies to: 208-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/federation-sdk/src/services/invite.service.spec.ts` around lines 67
- 69, Extract the inline EventService stub into a named constant (e.g., const
mockEventService = { notify: () => Promise.resolve() } as unknown as
EventService) and use that variable when constructing StateService (the existing
new StateService(...) call) to make the test setup clearer and reusable; do the
same for the other occurrences of the inline stub in this spec so all
StateService instantiations reference the shared mockEventService.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/federation-sdk/src/services/invite.service.ts`:
- Line 219: Prettier is failing on the if condition that uses an awaited call;
update the condition in invite.service.ts so the awaited expression is
parenthesized: change the line using createEvent and await
this.canResolveEventState(inviteEvent) to use (await
this.canResolveEventState(inviteEvent)) so the condition reads like if
(createEvent && (await this.canResolveEventState(inviteEvent))) { — this fixes
the formatting complaint while preserving logic in the createEvent /
canResolveEventState / inviteEvent check.
- Around line 239-252: canResolveEventState currently treats outliers as "found"
because findByIds includes events with stateId == '', and it also ignores
missing auth events; update canResolveEventState to fetch prev_event ids and
auth_event ids (use event.getPreviousEventIds() and event.getAuthEventIds()),
load both sets via eventRepository.findByIds(...).toArray(), and only return
true if the number of loaded prev events equals prev_event_ids.length AND the
number of loaded auth events equals auth_event_ids.length AND none of the loaded
events have empty or falsy stateId (i.e., fully materialized); additionally, in
handlePdu (where _resolveStateAtEvent is invoked), wrap the state
resolution/auth checks in a try/catch and on failure call
insertOutlierEvent(...) for the invite event so missing/malformed dependencies
fall back to storing an outlier instead of throwing.

In `@packages/federation-sdk/src/services/state.service.ts`:
- Around line 419-425: The code computes knownEventIds from
store.getEvents(allEventIds) after persisting the batch (including the newly
created m.room.create), which causes the just-created create event
(createEvent.eventId) to be treated as already-known and suppresses notify() —
fix processInitialState by fetching pre-existing event IDs before any writes
(compute knownEventIds from store.getEvents(allEventIds) prior to persisting the
batch) or, if simpler, explicitly remove createEvent.eventId from the
knownEventIds set after the fetch (ensure the guard that checks knownEventIds
before calling notify() uses the pre-write snapshot); reference sortedEvents,
allEventIds, store.getEvents, knownEventIds, createEvent and notify to locate
and update the logic.

---

Nitpick comments:
In `@packages/federation-sdk/src/services/invite.service.spec.ts`:
- Around line 67-69: Extract the inline EventService stub into a named constant
(e.g., const mockEventService = { notify: () => Promise.resolve() } as unknown
as EventService) and use that variable when constructing StateService (the
existing new StateService(...) call) to make the test setup clearer and
reusable; do the same for the other occurrences of the inline stub in this spec
so all StateService instantiations reference the shared mockEventService.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8075002b-41f9-45b8-9ed6-32916e6be4b0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ddcf68 and 40038d2.

📒 Files selected for processing (4)
  • packages/federation-sdk/src/services/invite.service.spec.ts
  • packages/federation-sdk/src/services/invite.service.ts
  • packages/federation-sdk/src/services/room.service.ts
  • packages/federation-sdk/src/services/state.service.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-10T22:18:31.655Z
Learnt from: sampaiodiego
Repo: RocketChat/homeserver PR: 224
File: packages/federation-sdk/src/services/event-authorization.service.ts:261-268
Timestamp: 2025-10-10T22:18:31.655Z
Learning: In packages/federation-sdk/src/services/state.service.ts, the method `StateService.getLatestRoomState(roomId: string)` has return type `Promise<State>` and never returns undefined. If the state is not found, it throws an error with message "No state found for room ${roomId}" instead of returning undefined.

Applied to files:

  • packages/federation-sdk/src/services/state.service.ts
  • packages/federation-sdk/src/services/room.service.ts
  • packages/federation-sdk/src/services/invite.service.ts
🪛 ESLint
packages/federation-sdk/src/services/invite.service.ts

[error] 219-219: Replace await·this.canResolveEventState(inviteEvent with (await·this.canResolveEventState(inviteEvent)

(prettier/prettier)

🪛 GitHub Actions: my-workflow
packages/federation-sdk/src/services/invite.service.ts

[error] 219-219: prettier/prettier failed: Replace await·this.canResolveEventState(inviteEvent with (await·this.canResolveEventState(inviteEvent)

🔇 Additional comments (2)
packages/federation-sdk/src/services/room.service.ts (1)

734-750: LGTM — always replaying send_join state on re-join is the right control flow.

The narrowed UnknownRoomError catch avoids swallowing unrelated failures, and unconditionally calling processInitialState(state, authChain) matches the leave/re-join case this PR is fixing.

packages/federation-sdk/src/services/invite.service.spec.ts (1)

301-495: Nice regression coverage for the re-invite/outlier paths.

These cases exercise the exact combinations the production code now branches on: unknown prev_events, known prev_events, missing m.room.create, and the full invite → join → leave → re-invite cycle.

Comment on lines +239 to +252
/**
* Checks whether the invite event's prev_events exist locally so that
* handlePdu can resolve the state at the event. When the local server
* left a room and missed events, the invite's prev_events will reference
* events we never received, making state resolution impossible.
*/
private async canResolveEventState(event: PersistentEventBase): Promise<boolean> {
const prevEventIds = event.getPreviousEventIds();
if (prevEventIds.length === 0) {
return true;
}

const found = await this.eventRepository.findByIds(prevEventIds).toArray();
return found.length === prevEventIds.length;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

canResolveEventState() still green-lights handlePdu() too early.

findByIds() counts outliers as "found", but insertOutlierEvent() stores them with stateId: '', so _resolveStateAtEvent() still cannot build state from those dependencies. handlePdu() also performs an auth check from event.getAuthEventIds() before state resolution, so missing auth events can still make the branch at Line 219 throw instead of falling back to insertOutlierEvent().

Please only return true for fully materialized prev/auth dependencies, or catch that failure and store the invite as an outlier.

🛡️ One way to tighten the preflight
 	private async canResolveEventState(event: PersistentEventBase): Promise<boolean> {
-		const prevEventIds = event.getPreviousEventIds();
-		if (prevEventIds.length === 0) {
+		const dependencyIds = [...new Set([...event.getPreviousEventIds(), ...event.getAuthEventIds()])];
+		if (dependencyIds.length === 0) {
 			return true;
 		}
 
-		const found = await this.eventRepository.findByIds(prevEventIds).toArray();
-		return found.length === prevEventIds.length;
+		const found = await this.eventRepository.findByIds(dependencyIds).toArray();
+		const records = new Map(found.map((record) => [record._id, record]));
+
+		return dependencyIds.every((id) => {
+			const record = records.get(id);
+			return record && !record.outlier && Boolean(record.stateId);
+		});
 	}
📝 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
/**
* Checks whether the invite event's prev_events exist locally so that
* handlePdu can resolve the state at the event. When the local server
* left a room and missed events, the invite's prev_events will reference
* events we never received, making state resolution impossible.
*/
private async canResolveEventState(event: PersistentEventBase): Promise<boolean> {
const prevEventIds = event.getPreviousEventIds();
if (prevEventIds.length === 0) {
return true;
}
const found = await this.eventRepository.findByIds(prevEventIds).toArray();
return found.length === prevEventIds.length;
/**
* Checks whether the invite event's prev_events exist locally so that
* handlePdu can resolve the state at the event. When the local server
* left a room and missed events, the invite's prev_events will reference
* events we never received, making state resolution impossible.
*/
private async canResolveEventState(event: PersistentEventBase): Promise<boolean> {
const dependencyIds = [...new Set([...event.getPreviousEventIds(), ...event.getAuthEventIds()])];
if (dependencyIds.length === 0) {
return true;
}
const found = await this.eventRepository.findByIds(dependencyIds).toArray();
const records = new Map(found.map((record) => [record._id, record]));
return dependencyIds.every((id) => {
const record = records.get(id);
return record && !record.outlier && Boolean(record.stateId);
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/federation-sdk/src/services/invite.service.ts` around lines 239 -
252, canResolveEventState currently treats outliers as "found" because findByIds
includes events with stateId == '', and it also ignores missing auth events;
update canResolveEventState to fetch prev_event ids and auth_event ids (use
event.getPreviousEventIds() and event.getAuthEventIds()), load both sets via
eventRepository.findByIds(...).toArray(), and only return true if the number of
loaded prev events equals prev_event_ids.length AND the number of loaded auth
events equals auth_event_ids.length AND none of the loaded events have empty or
falsy stateId (i.e., fully materialized); additionally, in handlePdu (where
_resolveStateAtEvent is invoked), wrap the state resolution/auth checks in a
try/catch and on failure call insertOutlierEvent(...) for the invite event so
missing/malformed dependencies fall back to storing an outlier instead of
throwing.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 28.26087% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.72%. Comparing base (3ddcf68) to head (2658e25).

Files with missing lines Patch % Lines
...ages/federation-sdk/src/services/invite.service.ts 5.26% 18 Missing ⚠️
...kages/federation-sdk/src/services/state.service.ts 46.66% 8 Missing ⚠️
...ckages/federation-sdk/src/services/room.service.ts 41.66% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #391      +/-   ##
==========================================
- Coverage   50.83%   50.72%   -0.12%     
==========================================
  Files         101      101              
  Lines       11475    11512      +37     
==========================================
+ Hits         5833     5839       +6     
- Misses       5642     5673      +31     

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

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

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.

2 participants