fix: enhance invite handling for re-join scenarios and prevent duplicate events#391
fix: enhance invite handling for re-join scenarios and prevent duplicate events#391
Conversation
WalkthroughInvites are now stored as outliers unless a local Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ 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. Comment |
40038d2 to
3812d75
Compare
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/federation-sdk/src/services/invite.service.spec.ts (1)
67-69: Hoist theEventServicestub to a variable for cleaner test setup.The spec files are excluded from TypeScript compilation (
tsconfig.jsonexcludes**/*.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
📒 Files selected for processing (4)
packages/federation-sdk/src/services/invite.service.spec.tspackages/federation-sdk/src/services/invite.service.tspackages/federation-sdk/src/services/room.service.tspackages/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.tspackages/federation-sdk/src/services/room.service.tspackages/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 replayingsend_joinstate on re-join is the right control flow.The narrowed
UnknownRoomErrorcatch avoids swallowing unrelated failures, and unconditionally callingprocessInitialState(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, knownprev_events, missingm.room.create, and the full invite → join → leave → re-invite cycle.
3812d75 to
2658e25
Compare
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
|
/jira FGA-42 |
1 similar comment
|
/jira FGA-42 |
a0ea5cf to
7558235
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
packages/federation-sdk/src/services/invite.service.spec.tspackages/federation-sdk/src/services/invite.service.tspackages/federation-sdk/src/services/room.service.tspackages/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.createfixes the false “already known” case on first joins while still deduping re-joins.
| const existingEvents = await store.getEvents(allEventIds); | ||
| const knownEventIds = new Set(existingEvents.map((e) => e.eventId)); |
There was a problem hiding this comment.
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.
FGA-57
Summary by CodeRabbit
Bug Fixes
Tests