Skip to content

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

Merged
ggazzo merged 4 commits intomainfrom
invite-after-leave
Mar 31, 2026
Merged

fix: enhance invite handling for re-join scenarios and prevent duplicate events#391
ggazzo merged 4 commits intomainfrom
invite-after-leave

Conversation

@sampaiodiego
Copy link
Copy Markdown
Member

@sampaiodiego sampaiodiego commented Mar 27, 2026

FGA-57

Summary by CodeRabbit

  • Bug Fixes

    • Avoid duplicate notifications for already-known room events; only new events trigger notifications.
    • Treat invites as outliers when required room state or referenced prior events cannot be resolved locally.
    • Ensure re-join flows refresh and apply initial room state for existing rooms.
  • Tests

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Walkthrough

Invites are now stored as outliers unless a local m.room.create exists and all referenced prev/auth events are locally resolvable; state initialization preloads known event IDs to avoid re-notifying already-seen events. Remote join always runs processInitialState (re-join path refreshed).

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 now requires a local m.room.create and uses canResolveEventState(...) to decide between stateService.handlePdu and eventRepository.insertOutlierEvent. Adds a Bun/Mongo integration-style test suite covering outlier cases and invite→join→leave→reinvite cycles.
State initialization
packages/federation-sdk/src/services/state.service.ts
processInitialState creates the EventStore earlier, preloads persisted event IDs from DB (authChain + event cache) into knownEventIds, and skips eventService.notify for events already known while still creating state deltas and updating the room graph.
Join flow
packages/federation-sdk/src/services/room.service.ts
RoomService.joinUser now rethrows non-UnknownRoomError, logs only on UnknownRoomError, and unconditionally calls stateService.processInitialState(state, authChain) after the version check so existing rooms are refreshed/updated with send_join state.

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 decision
    InviteService->>EventRepository: check for room-create
    InviteService->>EventRepository: fetch prev/auth events (canResolveEventState)
    alt room-create exists AND prev/auth events resolvable (non-empty stateId)
        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

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: enhancing invite handling for re-join scenarios and preventing duplicate events, which aligns with the core modifications across invite.service.ts, state.service.ts, and room.service.ts.
Linked Issues check ✅ Passed The PR changes address FGA-57 by fixing invite handling after kicks: invite.service.ts now stores invites as outliers when state cannot be resolved, state.service.ts prevents duplicate notifications on re-joins by tracking known events, and room.service.ts processes initial state on both first-join and re-join scenarios, enabling the second invite to work correctly even with intervening events.
Out of Scope Changes check ✅ Passed All changes are scoped to addressing FGA-57: test coverage, outlier handling for unresolvable invites, duplicate prevention in notifications, and state processing for re-joins are all directly related to the issue and its resolution.

✏️ 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.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 22.41379% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.63%. Comparing base (1bba45f) to head (7558235).

Files with missing lines Patch % Lines
...ages/federation-sdk/src/services/invite.service.ts 3.22% 30 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.80%   50.63%   -0.17%     
==========================================
  Files         101      101              
  Lines       11482    11531      +49     
==========================================
+ Hits         5833     5839       +6     
- Misses       5649     5692      +43     

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

ggazzo
ggazzo previously approved these changes Mar 30, 2026
@sampaiodiego
Copy link
Copy Markdown
Member Author

/jira FGA-42

1 similar comment
@sampaiodiego
Copy link
Copy Markdown
Member Author

/jira FGA-42

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/state.service.ts`:
- Around line 395-396: The current dedupe uses store.getEvents and knownEventIds
which only proves a DB row exists and can skip notify() if notify previously
failed; change dedupe to rely on a persisted "notified"/outbox marker instead of
raw event presence: update the event creation path (where addToRoomGraph() and
notify() are called) to write an event row with a notified=false/outbox entry,
perform addToRoomGraph(), call notify(), and only after notify() succeeds set
notified=true (or delete/mark the outbox entry) so retries will re-run notify
when needed; update the lookup that currently builds knownEventIds (and the same
code at the other occurrence you noted) to check the notified flag/outbox table
rather than existence, or use a transactional outbox pattern to guarantee
atomicity of graph insertion vs notification dispatch.
🪄 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: 9274b094-98ad-45c6-916e-fcb980458cef

📥 Commits

Reviewing files that changed from the base of the PR and between a0ea5cf and 7558235.

📒 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 (2)
  • packages/federation-sdk/src/services/room.service.ts
  • packages/federation-sdk/src/services/invite.service.ts
📜 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/state.service.ts
🔇 Additional comments (1)
packages/federation-sdk/src/services/state.service.ts (1)

392-394: Nice pre-write snapshot.

Running the lookup before persisting m.room.create fixes the false “already known” case on first joins while still deduping re-joins.

Comment on lines +395 to +396
const existingEvents = await store.getEvents(allEventIds);
const knownEventIds = new Set(existingEvents.map((e) => e.eventId));
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

Don't use DB presence as the notification dedupe signal.

knownEventIds only proves the row already exists. If addToRoomGraph() succeeds and notify() then throws, a retry will permanently skip the notify() side effects in packages/federation-sdk/src/services/event.service.ts:817-1014 even though they never completed. Please dedupe on a persisted “notified”/outbox marker instead of raw event existence.

Also applies to: 463-465

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

In `@packages/federation-sdk/src/services/state.service.ts` around lines 395 -
396, The current dedupe uses store.getEvents and knownEventIds which only proves
a DB row exists and can skip notify() if notify previously failed; change dedupe
to rely on a persisted "notified"/outbox marker instead of raw event presence:
update the event creation path (where addToRoomGraph() and notify() are called)
to write an event row with a notified=false/outbox entry, perform
addToRoomGraph(), call notify(), and only after notify() succeeds set
notified=true (or delete/mark the outbox entry) so retries will re-run notify
when needed; update the lookup that currently builds knownEventIds (and the same
code at the other occurrence you noted) to check the notified flag/outbox table
rather than existence, or use a transactional outbox pattern to guarantee
atomicity of graph insertion vs notification dispatch.

@ggazzo ggazzo merged commit 38ff1ff into main Mar 31, 2026
3 checks passed
@ggazzo ggazzo deleted the invite-after-leave branch March 31, 2026 13:22
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.

3 participants