Skip to content

Conversation

@ggazzo
Copy link
Member

@ggazzo ggazzo commented Sep 9, 2025

Summary by CodeRabbit

  • New Features

    • Added Matrix federation endpoints: GET /_matrix/federation/v1/state_ids/:roomId and GET /_matrix/federation/v1/state/:roomId to retrieve state at a specific event, including auth chain.
  • Improvements

    • More accurate room state reconstruction around events, including proper handling of room creation and fallback paths when stored state is missing.
    • Enhanced logging and clearer error responses to aid troubleshooting during state resolution.

@codecov-commenter
Copy link

codecov-commenter commented Sep 9, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.42%. Comparing base (7a737f7) to head (23f2ec1).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #171   +/-   ##
=======================================
  Coverage   80.42%   80.42%           
=======================================
  Files          58       58           
  Lines        4516     4516           
=======================================
  Hits         3632     3632           
  Misses        884      884           

☔ 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 force-pushed the chore/state-ids-for-all-events branch from d351d1b to bcbae69 Compare September 9, 2025 22:19
@ggazzo ggazzo marked this pull request as ready for review September 10, 2025 03:49
@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from 374df87 to ba7cbd2 Compare September 12, 2025 03:59
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

Walkthrough

Introduces federation state endpoints and underlying state resolution. Adds EventService methods to return state and state_ids at an event. Overhauls StateService to reconstruct room state at/before events, adds new builders and APIs, and uses stateId when persisting events. Registers a new homeserver controller plugin.

Changes

Cohort / File(s) Summary
State reconstruction overhaul
packages/federation-sdk/src/services/state.service.ts
Replaces findStateBeforeEvent with findStateAtEvent; adds multiple builders for reconstructing state from mappings/deltas/events; adds getFullRoomStateAndStateId, getFullRoomState2, getFullRoomStateBeforeEvent2; updates getFullRoomState return type; adds logging and stricter error handling; imports StateStore type.
Federation state resolution APIs
packages/federation-sdk/src/services/event.service.ts
Adds getStateIds(roomId, eventId) and getState(roomId, eventId); validates event-room match; derives state at event; computes pdu ids and full auth chain via getAuthChain; returns pdus/auth_chain or ids; warns on per-event failures.
Homeserver federation state API
packages/homeserver/src/controllers/federation/state.controller.ts, packages/homeserver/src/homeserver.module.ts
Adds statePlugin with GET /_matrix/federation/v1/state_ids/:roomId and GET /_matrix/federation/v1/state/:roomId; wires DTOs and error responses; registers plugin in homeserver module.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant HS as Federation Client
  participant H as Homeserver (Elysia)
  participant ES as EventService
  participant SS as StateService
  participant Repo as State/Room Store

  HS->>H: GET /_matrix/federation/v1/state_ids/:roomId?event_id=E
  H->>ES: getStateIds(roomId, E)
  ES->>Repo: fetch event E
  ES->>SS: findStateAtEvent(E)
  SS->>Repo: load state snapshot / mappings / deltas
  SS-->>ES: Map<StateKey, Event>
  ES->>Repo: get room version and store
  loop For each state event
    ES->>Repo: resolve getAuthChain(event)
    Note over ES: Collect pdu_ids and auth_chain_ids<br/>(skip on per-event errors)
  end
  ES-->>H: { pdu_ids, auth_chain_ids }
  H-->>HS: 200 OK

  HS->>H: GET /_matrix/federation/v1/state/:roomId?event_id=E
  H->>ES: getState(roomId, E)
  ES->>Repo: fetch event E
  ES->>SS: findStateAtEvent(E)
  SS-->>ES: Map<StateKey, Event>
  ES->>Repo: resolve getAuthChain for all
  ES->>Repo: fetch auth events by ids
  ES-->>H: { pdus, auth_chain }
  H-->>HS: 200 OK
Loading
sequenceDiagram
  autonumber
  participant ES as EventService
  participant SS as StateService
  participant Repo as StateRepository
  participant RV as RoomVersion Resolver

  ES->>SS: findStateAtEvent(eventId)
  SS->>Repo: get raw event by id
  SS->>SS: build PDU from raw
  alt event is m.room.create
    SS-->>ES: empty state map
  else
    SS->>Repo: get stateId / mappings / deltas
    SS->>RV: get room version
    opt No stored state mappings
      SS->>Repo: reconstruct from stored deltas/events
    end
    SS->>SS: buildFullRoomStateFromStateMappings(...)
    SS-->>ES: Map<StateKey, PersistentEventBase>
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit taps the state machine’s gate,
Mapping keys to truths before an event’s fate.
PDU pebbles, auth-chain trails,
Hops through rooms, reconstructs details.
Two fresh endpoints, ears held high—
“State or IDs?” comes the reply.
Thump-thump: consistency, bye!

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/state-ids-for-all-events

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

Pre-merge checks

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 "chore: set stateid for all events" is concise and accurately captures the primary intent of the changeset—ensuring persisted events include a stateId—which aligns with the state reconstruction and persistence updates in the diff. It is specific, developer-focused, and readable for teammates scanning history.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

ricardogarim
ricardogarim previously approved these changes Sep 12, 2025
@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from a0a52cb to 27983d5 Compare September 13, 2025 19:23
@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from 27983d5 to 5b6f94b Compare September 13, 2025 19:29
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from 55e2213 to 517bd5e Compare September 13, 2025 23:02
Copy link
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: 0

♻️ Duplicate comments (2)
packages/federation-sdk/src/services/state.service.ts (2)

221-307: Potential issue: Incorrect "before event" semantics and mixing stateId with eventId.


414-452: Fix inconsistent "latest"/"last" state retrieval (sort-order mismatch).

🧹 Nitpick comments (3)
packages/federation-sdk/src/services/state.service.ts (3)

322-333: Suggest using more descriptive method name.

The method buildLastRoomState retrieves all state mappings for a room and reconstructs the complete room state. Consider renaming to buildFullRoomStateFromAllMappings or similar to better convey its comprehensive nature.

-private async buildLastRoomState(roomId: string): Promise<State> {
+private async buildFullRoomStateFromAllMappings(roomId: string): Promise<State> {

Also update the call site at line 449:

-state: await this.buildLastRoomState(roomId),
+state: await this.buildFullRoomStateFromAllMappings(roomId),

381-406: Suggest batching event retrieval to avoid N+1 queries.

The current implementation fetches events one by one in a loop, causing N+1 database queries. Consider batching the event retrieval for better performance.

 private async buildFullRoomStateStoredEvents(
   events: Map<StateMapKey, string>,
   roomVersion: RoomVersion,
 ): Promise<Map<StateMapKey, PersistentEventBase>> {
   const finalState = new Map<StateMapKey, PersistentEventBase>();

+  // Batch fetch all events at once
+  const eventIds = Array.from(events.values());
+  const eventsCursor = this.eventRepository.findByIds(eventIds);
+  const eventsArray = await eventsCursor.toArray();
+  const eventsById = new Map(eventsArray.map(e => [e._id, e]));
+
-  for (const [stateKey, eventId] of events) {
-    const event = await this.eventRepository.findById(eventId);
+  for (const [stateKey, eventId] of events) {
+    const event = eventsById.get(eventId);
     if (!event) {
       throw new Error('Event not found');
     }

460-460: Fix method call after refactoring.

The call should be updated to use the new findStateBeforeEvent method instead of the old findStateAroundEvent.

-const state = await this.findStateAroundEvent(eventId);
+const state = await this.findStateBeforeEvent(eventId);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55e2213 and 234ea6d.

📒 Files selected for processing (2)
  • packages/federation-sdk/src/repositories/state.repository.ts (1 hunks)
  • packages/federation-sdk/src/services/state.service.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/federation-sdk/src/repositories/state.repository.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/state.service.ts (4)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/factory.ts (1)
  • PersistentEventFactory (37-968)
packages/federation-sdk/src/repositories/state.repository.ts (1)
  • StateStore (14-25)
packages/room/src/manager/room-state.ts (1)
  • RoomState (15-154)
🔇 Additional comments (8)
packages/federation-sdk/src/services/state.service.ts (8)

16-16: LGTM: Clean import of StateStore type.

The StateStore type import enables proper typing for the new state reconstruction builders.


209-219: LGTM: Helper method for state chain traversal.

The findPreviousStateId method correctly retrieves the last previous state ID from the chain, with proper error handling.


335-349: LGTM: Well-structured state reconstruction from single mapping.

The method correctly combines the current state mapping with its previous states to reconstruct the full room state.


351-363: LGTM: Clean pipeline approach for state reconstruction.

The method follows a clear pipeline: mappings → event IDs → persistent events, with proper separation of concerns.


365-379: LGTM: Efficient state mapping to event ID conversion.

The method correctly builds a map of state keys to event IDs from state mappings, with proper validation for missing deltas.


408-412: LGTM: Clean delegation to new state retrieval method.

The refactored getFullRoomState properly delegates to getFullRoomStateAndStateId and extracts only the state portion.


848-850: LGTM: Proper stateId propagation for timeline events.

The change correctly retrieves both state and stateId, ensuring the stateId is properly set when persisting timeline events instead of using an empty string.


910-910: LGTM: StateId now properly set for timeline events.

The timeline event persistence now uses the retrieved stateId instead of an empty string, which aligns with the PR objective to set stateId for all events.

coderabbitai bot added a commit that referenced this pull request Sep 15, 2025
Docstrings generation was requested by @ggazzo.

* #171 (comment)

The following files were modified:

* `packages/homeserver/src/homeserver.module.ts`
* `packages/room/src/state_resolution/definitions/definitions.ts`
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Note

Generated docstrings for this pull request at #194

@ggazzo ggazzo changed the base branch from main to chore/get-auth-chain September 15, 2025 12:45
@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from 234ea6d to da3ba13 Compare September 15, 2025 12:46
Copy link
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: 2

♻️ Duplicate comments (1)
packages/federation-sdk/src/services/state.service.ts (1)

261-276: Incorrect “before event” semantics when there is no previous state

When prevStateIds.length === 0, this path unconditionally adds the current event to the returned “before” snapshot. It should return an empty state unless includeEvent is true.

-    // TODO: not sure which case is this
-    if (storedState.prevStateIds.length === 0) {
-      const previous = await this.eventRepository.findById(
-        storedState.delta.eventId,
-      );
-      if (!previous) {
-        throw new Error(`Event ${eventId} not found`);
-      }
-
-      state.set(
-        storedState.delta.identifier,
-        PersistentEventFactory.createFromRawEvent(previous.event, roomVersion),
-      );
-
-      return state;
-    }
+    // No previous state recorded -> "state before event" has no entry for this key,
+    // unless includeEvent is true.
+    if (storedState.prevStateIds.length === 0) {
+      if (includeEvent) {
+        const current = await this.eventRepository.findById(
+          storedState.delta.eventId,
+        );
+        if (!current) {
+          throw new Error(`Event ${storedState.delta.eventId} not found`);
+        }
+        state.set(
+          storedState.delta.identifier as StateMapKey,
+          PersistentEventFactory.createFromRawEvent(current.event, roomVersion),
+        );
+      }
+      return state;
+    }

Follow-up: the same edge case exists in findStateAroundEvent(...) at Lines 156-171 — please apply the same guard there.

🧹 Nitpick comments (5)
packages/federation-sdk/src/services/state.service.ts (5)

249-252: Fix logger call shape

logger.error should get a context object first, not a string. Use { eventId } for structured logs.

- if (!storedState) {
-   this.logger.error(eventId, 'last state delta not found');
+ if (!storedState) {
+   this.logger.error({ eventId }, 'last state delta not found');

278-301: Eliminate N+1 event lookups; clarify variable naming

  • Batch-fetch events referenced by the mappings to avoid 1 query per state key.
  • eventsToFetch holds state IDs, not event IDs — rename to stateIdsToFetch for clarity.
-    const eventsToFetch = [
-      includeEvent && storedState._id.toString(),
-      ...storedState.prevStateIds,
-    ].filter(Boolean) as string[];
-
-    const stateMappings = await this.stateRepository
-      .getStateMappingsByStateIdsOrdered(eventsToFetch)
-      .toArray();
-
-    for await (const { delta } of stateMappings) {
-      const { identifier: stateKey, eventId } = delta;
-      const event = await this.eventRepository.findById(eventId);
-      if (!event) {
-        throw new Error(`Event ${eventId} not found`);
-      }
-      state.set(
-        stateKey as StateMapKey,
-        PersistentEventFactory.createFromRawEvent(event.event, roomVersion),
-      );
-    }
+    const stateIdsToFetch = [
+      includeEvent && storedState._id.toString(),
+      ...storedState.prevStateIds,
+    ].filter(Boolean) as string[];
+
+    const stateMappings = await this.stateRepository
+      .getStateMappingsByStateIdsOrdered(stateIdsToFetch)
+      .toArray();
+
+    const eventIds = stateMappings.map(({ delta }) => delta.eventId);
+    const eventsCursor = this.eventRepository.findByIds(eventIds);
+    const events = await eventsCursor.toArray();
+    const eventsById = new Map(events.map((e) => [e._id, e]));
+
+    for (const { delta } of stateMappings) {
+      const { identifier: stateKey, eventId } = delta;
+      const eventDoc = eventsById.get(eventId);
+      if (!eventDoc) {
+        throw new Error(`Event ${eventId} not found`);
+      }
+      state.set(
+        stateKey as StateMapKey,
+        PersistentEventFactory.createFromRawEvent(eventDoc.event, roomVersion),
+      );
+    }

316-327: Fallback path is O(N) over room history — gate and measure

This rebuild scans all mappings ascending; fine as a fallback, but add a counter/metric to track usage and consider a feature flag to disable in prod.


375-399: Hydration does N+1 DB calls; batch by IDs

Batch findByIds for all eventIds and index them to avoid per-key lookups.

-  for (const [stateKey, eventId] of events) {
-    const event = await this.eventRepository.findById(eventId);
-    if (!event) {
-      throw new Error('Event not found');
-    }
-    const pdu = PersistentEventFactory.createFromRawEvent(
-      event.event,
-      roomVersion,
-    );
-    if (pdu.eventId !== eventId) {
-      throw new Error('Event id mismatch while building room state');
-    }
-    finalState.set(stateKey, pdu);
-  }
+  const allEventIds = Array.from(events.values());
+  const cursor = this.eventRepository.findByIds(allEventIds);
+  const docs = await cursor.toArray();
+  const byId = new Map(docs.map((d) => [d._id, d]));
+  for (const [stateKey, eventId] of events) {
+    const doc = byId.get(eventId);
+    if (!doc) {
+      throw new Error(`Event ${eventId} not found`);
+    }
+    const pdu = PersistentEventFactory.createFromRawEvent(doc.event, roomVersion);
+    if (pdu.eventId !== eventId) {
+      throw new Error('Event id mismatch while building room state');
+    }
+    finalState.set(stateKey, pdu);
+  }

454-455: Route to the stricter “before event” API

To avoid duplicating semantics and to respect the “before” contract (especially for rooms with no prior state), call findStateBeforeEvent(eventId) here.

- const state = await this.findStateAroundEvent(eventId);
+ const state = await this.findStateBeforeEvent(eventId);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 234ea6d and ec85ee5.

📒 Files selected for processing (10)
  • packages/federation-sdk/src/repositories/state.repository.ts (1 hunks)
  • packages/federation-sdk/src/services/event.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (6 hunks)
  • packages/homeserver/src/controllers/federation/event.controller.ts (1 hunks)
  • packages/homeserver/src/controllers/federation/profiles.controller.ts (2 hunks)
  • packages/homeserver/src/dtos/federation/profiles.dto.ts (1 hunks)
  • packages/homeserver/src/dtos/federation/state-ids.dto.ts (1 hunks)
  • packages/homeserver/src/dtos/federation/state.dto.ts (1 hunks)
  • packages/homeserver/src/dtos/index.ts (1 hunks)
  • packages/homeserver/src/homeserver.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/federation-sdk/src/repositories/state.repository.ts
  • packages/homeserver/src/dtos/federation/profiles.dto.ts
  • packages/homeserver/src/homeserver.module.ts
  • packages/homeserver/src/controllers/federation/event.controller.ts
  • packages/homeserver/src/dtos/federation/state.dto.ts
  • packages/homeserver/src/dtos/federation/state-ids.dto.ts
  • packages/homeserver/src/dtos/index.ts
  • packages/federation-sdk/src/services/event.service.ts
  • packages/homeserver/src/controllers/federation/profiles.controller.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/state.service.ts (4)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/factory.ts (1)
  • PersistentEventFactory (37-144)
packages/federation-sdk/src/repositories/state.repository.ts (1)
  • StateStore (14-25)
packages/room/src/manager/room-state.ts (1)
  • RoomState (15-154)
🔇 Additional comments (6)
packages/federation-sdk/src/services/state.service.ts (6)

22-22: Import of StateStore type looks correct

Type is used below in the builders; no issues.


329-343: Good use of typed StateStore and single-call expansion

Fetching [state._id, ...prevStateIds] then delegating to builders keeps concerns separated.


345-357: Builder composition LGTM

The split between event-id reconstruction and event hydration is clean.


402-406: Thin wrapper LGTM

Keeping a single source of truth in getFullRoomStateAndStateId is good.


860-863: Good: fetch state and stateId together for timeline events

Using getFullRoomStateAndStateId ensures the event is persisted against the correct stateId.


917-923: Correctly persist timeline event with derived stateId

Passing stateId ties the message to the exact room state snapshot at write time; this avoids orphaned events.

Comment on lines +359 to +373
private async buildFullRoomStateFromEvents(
stateMappings: StateStore[],
): Promise<Map<StateMapKey, string>> {
const state = new Map<StateMapKey, string>();
// first reconstruct the final state
for await (const stateMapping of stateMappings) {
if (!stateMapping.delta) {
throw new Error('State mapping has no delta');
}

if (!stateMapping.delta) {
throw new Error('State mapping delta is empty');
}
const { identifier: stateKey, eventId } = stateMapping.delta;

state.set(stateKey as StateMapKey, eventId);
state.set(stateKey, eventId);
}

return state;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Minor: avoid for-await over arrays

stateMappings is an array; use for (const ...) to avoid implying async iteration.

- for await (const stateMapping of stateMappings) {
+ for (const stateMapping of stateMappings) {

Also ensure getStateMappingsBy*Ordered returns mappings in ascending order so later entries override earlier ones deterministically.


🏁 Script executed:

#!/bin/bash
rg -nP -C2 'getStateMappingsByStateIdsOrdered|getStateMappingsByRoomIdOrderedAscending' packages/federation-sdk/src/repositories/state.repository.ts

Length of output: 233


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="packages/federation-sdk/src/repositories/state.repository.ts"
nl -ba "$file" | sed -n '40,90p'

Length of output: 40


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="packages/federation-sdk/src/repositories/state.repository.ts"
if [ -f "$file" ]; then
  awk '{printf "%5d: %s\n", NR, $0}' "$file"
else
  echo "MISSING: $file"
fi

Length of output: 3220


Minor: replace async iteration with regular for-of

stateMappings is typed as StateStore[] — use a plain loop instead of implying async iteration.

- for await (const stateMapping of stateMappings) {
+ for (const stateMapping of stateMappings) {

getStateMappingsByRoomIdOrdered and getStateMappingsByStateIdsOrdered already sort by createdAt ascending in packages/federation-sdk/src/repositories/state.repository.ts, so ordering is deterministic.

📝 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
private async buildFullRoomStateFromEvents(
stateMappings: StateStore[],
): Promise<Map<StateMapKey, string>> {
const state = new Map<StateMapKey, string>();
// first reconstruct the final state
for await (const stateMapping of stateMappings) {
if (!stateMapping.delta) {
throw new Error('State mapping has no delta');
}
if (!stateMapping.delta) {
throw new Error('State mapping delta is empty');
}
const { identifier: stateKey, eventId } = stateMapping.delta;
state.set(stateKey as StateMapKey, eventId);
state.set(stateKey, eventId);
}
return state;
}
private async buildFullRoomStateFromEvents(
stateMappings: StateStore[],
): Promise<Map<StateMapKey, string>> {
const state = new Map<StateMapKey, string>();
// first reconstruct the final state
for (const stateMapping of stateMappings) {
if (!stateMapping.delta) {
throw new Error('State mapping has no delta');
}
const { identifier: stateKey, eventId } = stateMapping.delta;
state.set(stateKey, eventId);
}
return state;
}
🤖 Prompt for AI Agents
In packages/federation-sdk/src/services/state.service.ts around lines 359 to
373, the method uses an async iterator ("for await (const stateMapping of
stateMappings)") even though stateMappings is typed as a plain StateStore[];
replace the async iteration with a regular for-of loop ("for (const stateMapping
of stateMappings)") and remove the unnecessary async modifier from the function
signature (and any unused awaits) so the function is a normal synchronous
function that returns a Promise immediately.

@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from ec85ee5 to 322bb64 Compare September 15, 2025 13:02
@ggazzo ggazzo force-pushed the chore/get-auth-chain branch from ac21e3f to 4a41834 Compare September 15, 2025 13:08
Base automatically changed from chore/get-auth-chain to main September 15, 2025 13:12
Copy link
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

♻️ Duplicate comments (2)
packages/federation-sdk/src/services/state.service.ts (2)

359-373: Replace async iteration over an array with a plain loop.

stateMappings is an array; for-await implies async sources.

-    for await (const stateMapping of stateMappings) {
+    for (const stateMapping of stateMappings) {

215-276: Fix “before event” semantics when there is no previous state.

If prevStateIds is empty, you currently always include the current event’s delta in the “before” snapshot. That’s incorrect unless includeEvent is true. Also the error references eventId rather than storedState.delta.eventId.

Apply:

-    // TODO: not sure which case is this
-    if (storedState.prevStateIds.length === 0) {
-      const previous = await this.eventRepository.findById(
-        storedState.delta.eventId,
-      );
-      if (!previous) {
-        throw new Error(`Event ${eventId} not found`);
-      }
-
-      state.set(
-        storedState.delta.identifier,
-        PersistentEventFactory.createFromRawEvent(previous.event, roomVersion),
-      );
-
-      return state;
-    }
+    if (storedState.prevStateIds.length === 0) {
+      // No recorded previous state => state before this event has no entry for this key,
+      // unless the caller explicitly asks to include the event itself.
+      if (includeEvent) {
+        const current = await this.eventRepository.findById(
+          storedState.delta.eventId,
+        );
+        if (!current) {
+          throw new Error(`Event ${storedState.delta.eventId} not found`);
+        }
+        state.set(
+          storedState.delta.identifier as StateMapKey,
+          PersistentEventFactory.createFromRawEvent(current.event, roomVersion),
+        );
+      }
+      return state;
+    }
🧹 Nitpick comments (3)
packages/federation-sdk/src/services/state.service.ts (3)

278-301: Avoid N+1 lookups and keep includeEvent out of the stateIds query.

Batch-fetch events for prevStateIds, then, if includeEvent is true, append the last delta explicitly. Also replace async iteration over arrays.

-    const eventsToFetch = [
-      includeEvent && storedState._id.toString(),
-      ...storedState.prevStateIds,
-    ].filter(Boolean) as string[];
-
-    const stateMappings = await this.stateRepository
-      .getStateMappingsByStateIdsOrdered(eventsToFetch)
-      .toArray();
-
-    for await (const { delta } of stateMappings) {
-      const { identifier: stateKey, eventId } = delta;
-      const event = await this.eventRepository.findById(eventId);
-      if (!event) {
-        throw new Error(`Event ${eventId} not found`);
-      }
-
-      state.set(
-        stateKey as StateMapKey,
-        PersistentEventFactory.createFromRawEvent(event.event, roomVersion),
-      );
-    }
-
-    return state;
+    const stateMappings = await this.stateRepository
+      .getStateMappingsByStateIdsOrdered(storedState.prevStateIds)
+      .toArray();
+
+    // batch-fetch referenced events
+    const eventIds = stateMappings.map(({ delta }) => delta.eventId);
+    const eventsCursor = this.eventRepository.findByIds(eventIds);
+    const events = await eventsCursor.toArray();
+    const eventsById = new Map(events.map((e) => [e._id, e]));
+
+    for (const { delta } of stateMappings) {
+      const { identifier: stateKey, eventId: deltaEventId } = delta;
+      const eventDoc = eventsById.get(deltaEventId);
+      if (!eventDoc) {
+        throw new Error(`Event ${deltaEventId} not found`);
+      }
+      state.set(
+        stateKey as StateMapKey,
+        PersistentEventFactory.createFromRawEvent(eventDoc.event, roomVersion),
+      );
+    }
+
+    if (includeEvent) {
+      const { identifier: lastStateKey, eventId: lastStateEventId } =
+        storedState.delta;
+      const lastEvent = await this.eventRepository.findById(lastStateEventId);
+      if (!lastEvent) {
+        throw new Error(`Event ${lastStateEventId} not found`);
+      }
+      state.set(
+        lastStateKey as StateMapKey,
+        PersistentEventFactory.createFromRawEvent(lastEvent.event, roomVersion),
+      );
+    }
+
+    return state;

375-397: Batch-fetch events to avoid N+1 queries in a hot path.

buildFullRoomStateStoredEvents fetches each event individually. Batch once and index by _id.

-    for (const [stateKey, eventId] of events) {
-      const event = await this.eventRepository.findById(eventId);
-      if (!event) {
-        throw new Error('Event not found');
-      }
-
-      const pdu = PersistentEventFactory.createFromRawEvent(
-        event.event,
-        roomVersion,
-      );
-
-      if (pdu.eventId !== eventId) {
-        throw new Error('Event id mismatch while building room state');
-      }
-
-      finalState.set(stateKey, pdu);
-    }
+    const eventIds = Array.from(events.values());
+    const cursor = this.eventRepository.findByIds(eventIds);
+    const docs = await cursor.toArray();
+    const byId = new Map(docs.map((d) => [d._id, d]));
+
+    for (const [stateKey, eventId] of events) {
+      const doc = byId.get(eventId);
+      if (!doc) {
+        throw new Error(`Event ${eventId} not found`);
+      }
+      const pdu = PersistentEventFactory.createFromRawEvent(
+        doc.event,
+        roomVersion,
+      );
+      if (pdu.eventId !== eventId) {
+        throw new Error('Event id mismatch while building room state');
+      }
+      finalState.set(stateKey, pdu);
+    }

454-454: Avoid divergence: reuse findStateBeforeEvent for the RoomState wrapper.

This calls findStateAroundEvent (defaults to “before”) while a dedicated findStateBeforeEvent exists. Prefer routing both wrappers through a single helper to prevent drift.

-    const state = await this.findStateAroundEvent(eventId);
+    const state = await this.findStateBeforeEvent(eventId);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec85ee5 and b265ccc.

📒 Files selected for processing (4)
  • packages/federation-sdk/src/services/event.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (6 hunks)
  • packages/homeserver/src/controllers/federation/event.controller.ts (1 hunks)
  • packages/homeserver/src/homeserver.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/homeserver/src/homeserver.module.ts
  • packages/federation-sdk/src/services/event.service.ts
  • packages/homeserver/src/controllers/federation/event.controller.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/state.service.ts (4)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/factory.ts (1)
  • PersistentEventFactory (37-144)
packages/federation-sdk/src/repositories/state.repository.ts (1)
  • StateStore (14-25)
packages/room/src/manager/room-state.ts (1)
  • RoomState (15-154)
🔇 Additional comments (7)
packages/federation-sdk/src/services/state.service.ts (7)

22-22: LGTM: type import addition is correct.

Brings StateStore into scope for the new helpers.


329-343: LGTM: single mapping → full state path is sound.

Pulls the chain (self + prev) and reuses the shared builders.


345-357: LGTM: compose-from-multiple mappings reads well.

Clear separation between collecting eventIds and materializing PDUs.


402-406: LGTM: wrapper preserves API and leverages cached stateId path.


860-863: LGTM: persisting timeline uses resolved stateId.

Correctly attaches stateId from getFullRoomStateAndStateId before validation/persist.


922-922: LGTM: writes the computed stateId with the timeline event.


316-327: Confirmed: ascending order is correct.
getStateMappingsByRoomIdOrderedAscending uses this.collection.find({ roomId }).sort({ createdAt: 1 }), so it returns mappings oldest→newest and buildLastRoomState's assumption is valid.

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/federation-sdk/src/services/state.service.ts (1)

194-208: Performance concern: N+1 database queries.

The loop fetches events one by one, causing N+1 database queries. This should be optimized to batch fetch all required events.

Consider applying this optimization pattern from the later builder methods:

-    for await (const { delta } of stateMappings) {
-      const { identifier: stateKey, eventId } = delta;
-      const event = await this.eventRepository.findById(eventId);
-      if (!event) {
-        throw new Error(`Event ${eventId} not found`);
-      }
-      state.set(
-        stateKey,
-        PersistentEventFactory.createFromRawEvent(event.event, roomVersion),
-      );
-    }
+    // Batch fetch all events
+    const eventIds = stateMappings.map(({ delta }) => delta.eventId);
+    const eventsCursor = this.eventRepository.findByIds(eventIds);
+    const events = await eventsCursor.toArray();
+    const eventsById = new Map(events.map((e) => [e._id, e]));
+
+    for (const { delta } of stateMappings) {
+      const { identifier: stateKey, eventId } = delta;
+      const event = eventsById.get(eventId);
+      if (!event) {
+        throw new Error(`Event ${eventId} not found`);
+      }
+      state.set(
+        stateKey,
+        PersistentEventFactory.createFromRawEvent(event.event, roomVersion),
+      );
+    }
♻️ Duplicate comments (3)
packages/federation-sdk/src/services/state.service.ts (3)

158-175: Potential issue: including current event in "before event" state when prevStateIds is empty.

When prevStateIds.length === 0, this code includes the current event (storedState.delta.eventId) in the returned state. However, since this method is documented to return state "prior to the event," including the current event may be semantically incorrect for the first state change in a room.


266-280: Use regular for-of loop instead of for-await.

The stateMappings parameter is typed as StateStore[] (a regular array), so using for await is unnecessary and potentially misleading.


177-188: Logic error: mixing eventId with stateIds in query.

Line 186 pushes storedState._id.toString() (a stateId) when includeEvent is true, but getStateMappingsByStateIdsOrdered expects state IDs, not event IDs. This will cause incorrect query results.

Apply this diff to fix the logic:

-    const eventsToFetch = [
-      includeEvent && storedState._id.toString(),
-      ...storedState.prevStateIds,
-    ].filter(Boolean) as string[];
+    const eventsToFetch = storedState.prevStateIds;

Then handle includeEvent separately after processing the state mappings, similar to the pattern used in the later builder methods.

🧹 Nitpick comments (1)
packages/federation-sdk/src/repositories/state.repository.ts (1)

34-34: Remove debug logging from production code.

This console.log statement will pollute production logs and should be removed before merging to main. Consider using the proper logger if debugging information is needed.

Apply this diff to remove the debug logging:

-    console.log('stateId ->>>', stateId);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b265ccc and 8403766.

📒 Files selected for processing (5)
  • packages/federation-sdk/src/repositories/state.repository.ts (1 hunks)
  • packages/federation-sdk/src/services/event.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (8 hunks)
  • packages/homeserver/src/controllers/federation/event.controller.ts (1 hunks)
  • packages/homeserver/src/homeserver.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/homeserver/src/homeserver.module.ts
  • packages/federation-sdk/src/services/event.service.ts
  • packages/homeserver/src/controllers/federation/event.controller.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/state.service.ts (4)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/factory.ts (1)
  • PersistentEventFactory (37-144)
packages/federation-sdk/src/repositories/state.repository.ts (1)
  • StateStore (14-25)
packages/room/src/manager/room-state.ts (1)
  • RoomState (15-154)
🔇 Additional comments (13)
packages/federation-sdk/src/services/state.service.ts (13)

22-22: LGTM! Proper type import for state builder parameters.

The import of StateStore type enables proper typing for the new private state-builder methods, which improves type safety across the state reconstruction pipeline.


113-116: LGTM! Clear documentation of state semantics.

The JSDoc comment clarifies that this method returns state prior to the event, which is essential for understanding the validation context.


117-141: LGTM! Improved method naming and early return for create events.

The rename from findStateBeforeEvent to findStateAtEvent better reflects the method's purpose, and the early return for m.room.create events correctly handles the edge case where no prior state exists.


143-157: LGTM! Proper state retrieval and error handling.

The method correctly retrieves the stored state using the event's stateId and provides clear error messages when state is not found. The debug logging will help with troubleshooting state reconstruction issues.


223-234: LGTM! Well-structured private helper method.

The buildLastRoomState method follows good practices by validating room version and delegating to the appropriate builder method.


236-250: LGTM! Modular state reconstruction from single mapping.

This builder method properly reconstructs state from a single StateStore mapping and its previous state IDs, following a clear pipeline from mappings → events → persistent events.


252-264: LGTM! Clean delegation pattern.

The method follows good separation of concerns by delegating to specialized builder methods.


282-307: LGTM! Robust event validation and reconstruction.

The method properly validates that fetched events match expected IDs and handles the reconstruction pipeline correctly with appropriate error handling.


309-313: LGTM! Clean wrapper method.

The method provides a convenient wrapper around getFullRoomStateAndStateId for callers who only need the state.


315-353: LGTM! Well-structured state retrieval with fallback.

The method properly attempts to use stored state first and falls back to building the last room state when needed. The TODO comment indicates this fallback is intentionally kept for production stability during the transition period.


355-363: LGTM! Consistent wrapper methods.

Both getFullRoomState2 and getFullRoomStateBeforeEvent2 provide clean wrappers that convert from the internal Map<StateMapKey, PersistentEventBase> format to the RoomState class.


767-770: LGTM! Proper state ID attachment for timeline events.

The change to use getFullRoomStateAndStateId ensures that timeline events are persisted with the correct stateId, which aligns with the PR objective of setting state IDs for all events.


829-829: LGTM! Consistent state ID usage.

Using the stateId from the destructured result ensures timeline events are properly associated with their state context.

@ggazzo ggazzo force-pushed the chore/state-ids-for-all-events branch from 8403766 to 2675b3a Compare September 15, 2025 14:27
Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/federation-sdk/src/services/state.service.ts (2)

194-205: Performance: N+1 database queries in state reconstruction

The loop fetches events one by one from the database, which will cause performance issues with large state sets. Consider batch fetching all events.

Replace the N+1 queries with batch fetching:

+const eventIds = stateMappings.map(({ delta }) => delta.eventId);
+const eventsCursor = this.eventRepository.findByIds(eventIds);
+const events = await eventsCursor.toArray();
+const eventsById = new Map(events.map(e => [e._id, e]));
+
-for await (const { delta } of stateMappings) {
+for (const { delta } of stateMappings) {
   const { identifier: stateKey, eventId } = delta;
-  const event = await this.eventRepository.findById(eventId);
+  const event = eventsById.get(eventId);
   if (!event) {
     throw new Error(`Event ${eventId} not found`);
   }
   
   state.set(
     stateKey,
     PersistentEventFactory.createFromRawEvent(event.event, roomVersion),
   );
 }

288-304: Performance: N+1 database queries in buildFullRoomStateStoredEvents

Similar to the earlier issue, this method fetches events one by one. Batch fetch for better performance.

Batch fetch the events:

 private async buildFullRoomStateStoredEvents(
   events: Map<StateMapKey, string>,
   roomVersion: RoomVersion,
 ): Promise<Map<StateMapKey, PersistentEventBase>> {
   const finalState = new Map<StateMapKey, PersistentEventBase>();
+  
+  const eventIds = Array.from(events.values());
+  const eventsCursor = this.eventRepository.findByIds(eventIds);
+  const eventDocs = await eventsCursor.toArray();
+  const eventsById = new Map(eventDocs.map(e => [e._id, e]));

-  for (const [stateKey, eventId] of events) {
-    const event = await this.eventRepository.findById(eventId);
+  for (const [stateKey, eventId] of events) {
+    const event = eventsById.get(eventId);
     if (!event) {
       throw new Error('Event not found');
     }

     const pdu = PersistentEventFactory.createFromRawEvent(
       event.event,
       roomVersion,
     );

     if (pdu.eventId !== eventId) {
       throw new Error('Event id mismatch while building room state');
     }

     finalState.set(stateKey, pdu);
   }

   return finalState;
 }
♻️ Duplicate comments (1)
packages/federation-sdk/src/services/state.service.ts (1)

185-188: Potential type confusion: mixing state IDs with state mapping IDs

Line 186 uses storedState._id.toString() (the MongoDB ObjectId of the state mapping) when includeEvent is true, but getStateMappingsByStateIdsOrdered expects state IDs. This appears to be a logic error where the state mapping's ObjectId is incorrectly used as a state ID.

The state mapping's _id (ObjectId) should not be mixed with state IDs. Based on the pattern in buildFullRoomStateFromStateMapping (lines 240-245), you likely want:

 const eventsToFetch = [
-  includeEvent && storedState._id.toString(),
+  includeEvent && stateId,
   ...storedState.prevStateIds,
 ].filter(Boolean) as string[];

Note that stateId is already available from line 144 where it's destructured from the event.

🧹 Nitpick comments (3)
packages/federation-sdk/src/services/state.service.ts (3)

113-117: Documentation is slightly misleading

The JSDoc comment says "This is the state prior to the event" but the implementation actually includes the event itself for timeline events (when includeEvent = !pdu.isState() on line 177). Consider clarifying the documentation to reflect this behavior.

Update the documentation to be more precise:

 /**
  * Returns the state used to validate the event
- * This is the state prior to the event
+ * For state events: Returns the state prior to the event
+ * For timeline events: Returns the state including the event itself
  */

271-271: Remove unnecessary async iteration

The for await loop is unnecessary since stateMappings is already a resolved array (not an async iterable).

-for await (const stateMapping of stateMappings) {
+for (const stateMapping of stateMappings) {

338-342: Clean up commented production check

There's commented-out code that appears to be a production environment check. Either remove it or uncomment it with proper configuration.

Either remove the commented code or make it configurable:

-// TODO: we should be ok to remove the fallback scenario for production, but since we are already running in some places I wanted to keep it for now
-
-// if (process.env.NODE_ENV === 'development') {
-//   throw new Error('No stored state found, and we are in production');
-// }
+// TODO: Remove fallback scenario once all environments have migrated
+if (this.configService.requireStoredState) {
+  throw new Error('No stored state found');
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8403766 and 2675b3a.

📒 Files selected for processing (2)
  • packages/federation-sdk/src/services/event.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/federation-sdk/src/services/event.service.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/state.service.ts (5)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/event-wrapper.ts (3)
  • event (102-112)
  • stateKey (90-94)
  • roomId (72-74)
packages/room/src/manager/factory.ts (1)
  • PersistentEventFactory (37-144)
packages/federation-sdk/src/repositories/state.repository.ts (1)
  • StateStore (14-25)
packages/room/src/manager/room-state.ts (1)
  • RoomState (15-154)
🔇 Additional comments (3)
packages/federation-sdk/src/services/state.service.ts (3)

767-769: Good: State ID properly attached to timeline events

The change to use getFullRoomStateAndStateId and pass the stateId when persisting timeline events is correct and aligns with the PR objective.


641-641: No change required — findStateAtEvent returns the pre-event state for state events.

The method computes includeEvent = !pdu.isState(), so for the persistStateEvent path (a state event) the event is excluded; calling findStateAtEvent(lastEvent._id) therefore returns the state prior to the event and is correct.


160-175: Verify empty prevStateIds handling (packages/federation-sdk/src/services/state.service.ts:160-175) — createStateMapping accepts a default prevStateIds = [] and is invoked from this file, but I couldn't find where prevStateIds is populated; confirm the producer. If prevStateIds can legitimately be empty (initial state), keep this branch but add a clear comment and a unit test that reconstructs state via storedState.delta.eventId; if not, remove/simplify this special-case branch and ensure previous-state resolution is deterministic.

@ggazzo ggazzo changed the title chore: set stateid for all events feat: stateid for all events and state endpoints Sep 15, 2025
@ggazzo ggazzo requested a review from sampaiodiego September 15, 2025 15:46
Copy link
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

♻️ Duplicate comments (2)
packages/federation-sdk/src/services/state.service.ts (2)

173-190: Eliminate N+1 lookups and async iteration over arrays in findStateAtEvent.

Batch fetch events and use a plain for‑of loop. This path is hot.

-    const stateMappings = await this.stateRepository
-      .getStateMappingsByStateIdsOrdered(eventsToFetch)
-      .toArray();
-
-    for await (const { delta } of stateMappings) {
-      const { identifier: stateKey, eventId } = delta;
-      const event = await this.eventRepository.findById(eventId);
-      if (!event) {
-        throw new Error(`Event ${eventId} not found`);
-      }
-
-      state.set(
-        stateKey,
-        PersistentEventFactory.createFromRawEvent(event.event, roomVersion),
-      );
-    }
+    const stateMappings = await this.stateRepository
+      .getStateMappingsByStateIdsOrdered(eventsToFetch)
+      .toArray();
+
+    const eventIds = stateMappings.map(({ delta }) => delta.eventId);
+    const eventsCursor = this.eventRepository.findByIds(eventIds);
+    const events = await eventsCursor.toArray();
+    const eventsById = new Map(events.map((e) => [e._id, e]));
+
+    for (const { delta } of stateMappings) {
+      const { identifier: stateKey, eventId: deltaEventId } = delta;
+      const eventDoc = eventsById.get(deltaEventId);
+      if (!eventDoc) {
+        throw new Error(`Event ${deltaEventId} not found`);
+      }
+      state.set(
+        stateKey as StateMapKey,
+        PersistentEventFactory.createFromRawEvent(eventDoc.event, roomVersion),
+      );
+    }

254-260: Replace async iteration over plain arrays.

-    for await (const stateMapping of stateMappings) {
+    for (const stateMapping of stateMappings) {
🧹 Nitpick comments (6)
packages/homeserver/src/controllers/federation/state.controller.ts (2)

23-34: Expose 404/500 error schemas and map thrown errors to ErrorResponseDto.

Only 200/400 are declared. Service-level “not found”/unexpected errors will surface as 500 without a documented shape. Add 404/500 to response to keep OpenAPI in sync and help clients.

       response: {
-        400: ErrorResponseDto,
+        400: ErrorResponseDto,
+        404: ErrorResponseDto,
+        500: ErrorResponseDto,
       },
@@
       response: {
-        400: ErrorResponseDto,
+        400: ErrorResponseDto,
+        404: ErrorResponseDto,
+        500: ErrorResponseDto,
       },

Also applies to: 43-52


38-41: Drop the non‑null assertion; DTO already guarantees presence.

GetStateQueryDto requires event_id, so the “!” isn’t needed.

- ({ params, query }) =>
-   eventService.getState(params.roomId, query.event_id!),
+ ({ params, query }) =>
+   eventService.getState(params.roomId, query.event_id),
packages/federation-sdk/src/services/state.service.ts (4)

162-168: Clarify doc comment: “includeEvent” refers to last state mapping, not the timeline event.

The state for a timeline event should be the room state at that point; the event itself isn’t part of the state. Reword to avoid confusion.

-     * If the event is a state event, we don't include the event in the state, otherwise the state would be the new state
-     * computed because of the event
-     *
-     * If the event is a timeline event, we include the event in the state
+     * If the event is a state event, do not include its own mapping; we want the state prior to it.
+     * If the event is a timeline event, include the latest state mapping that existed at that time.

149-151: Structured logging object for error.

Pass a context object to logger for consistency with other calls.

-      this.logger.error(eventId, 'last state delta not found');
+      this.logger.error({ eventId }, 'last state delta not found');

271-287: Batch event loads when materializing stored events.

Avoid per-item findById; fetch all, index, then build.

-    for (const [stateKey, eventId] of events) {
-      const event = await this.eventRepository.findById(eventId);
-      if (!event) {
-        throw new Error('Event not found');
-      }
-
-      const pdu = PersistentEventFactory.createFromRawEvent(
-        event.event,
-        roomVersion,
-      );
-
-      if (pdu.eventId !== eventId) {
-        throw new Error('Event id mismatch while building room state');
-      }
-
-      finalState.set(stateKey, pdu);
-    }
+    const ids = Array.from(events.values());
+    const cursor = this.eventRepository.findByIds(ids);
+    const docs = await cursor.toArray();
+    const byId = new Map(docs.map((e) => [e._id, e]));
+
+    for (const [stateKey, eventId] of events) {
+      const doc = byId.get(eventId);
+      if (!doc) {
+        throw new Error('Event not found');
+      }
+      const pdu = PersistentEventFactory.createFromRawEvent(
+        doc.event,
+        roomVersion,
+      );
+      if (pdu.eventId !== eventId) {
+        throw new Error('Event id mismatch while building room state');
+      }
+      finalState.set(stateKey, pdu);
+    }

847-849: Tighten TODOs/comments (public API surface).

Clean up TODO phrasing to avoid ambiguous/internal notes in published code.

-    // TODO: i know thisd is overcomplicated
-    //but writing this comment while not remembering what exactkly it does while not wanting to get my brain to do it either
+    // TODO: Simplify/clarify this flow; current implementation is more complex than necessary.
@@
-      // TODO: why it must to end whit `:` ?
+      // NOTE: Identifiers for state events include a trailing ":" by convention.

Also applies to: 886-887

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2675b3a and 23f2ec1.

📒 Files selected for processing (3)
  • packages/federation-sdk/src/services/state.service.ts (8 hunks)
  • packages/homeserver/src/controllers/federation/state.controller.ts (1 hunks)
  • packages/homeserver/src/homeserver.module.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/homeserver/src/homeserver.module.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/homeserver/src/controllers/federation/state.controller.ts (3)
packages/homeserver/src/dtos/federation/state-ids.dto.ts (3)
  • GetStateIdsParamsDto (4-6)
  • GetStateIdsQueryDto (8-10)
  • GetStateIdsResponseDto (12-17)
packages/homeserver/src/dtos/common/error.dto.ts (1)
  • ErrorResponseDto (3-6)
packages/homeserver/src/dtos/federation/state.dto.ts (3)
  • GetStateParamsDto (4-6)
  • GetStateQueryDto (8-10)
  • GetStateResponseDto (12-19)
packages/federation-sdk/src/services/state.service.ts (4)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/factory.ts (1)
  • PersistentEventFactory (37-144)
packages/federation-sdk/src/repositories/state.repository.ts (1)
  • StateStore (14-25)
packages/room/src/manager/room-state.ts (1)
  • RoomState (15-154)
🔇 Additional comments (2)
packages/homeserver/src/controllers/federation/state.controller.ts (1)

14-21: LGTM: plugin wiring and DI are clean.

Route paths, DTO usage, and tsyringe resolution look correct.

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

750-753: LGTM: timeline persistence now attaches the correct stateId.

Persisting timeline events with the current room stateId fixes “empty stateId” issues downstream.

Also applies to: 812-813

Comment on lines +146 to 151
const storedState = await this.stateRepository.getStateById(stateId);

if (!lastStateDelta) {
if (!storedState) {
this.logger.error(eventId, 'last state delta not found');
throw new Error(`State at event ${eventId} not found`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle events persisted without a stateId (fallback by timestamp).

findStateAtEvent throws when stateId is empty/missing (e.g., conflicted state events stored without mapping). Use a fallback to the latest state mapping at/before the event timestamp.

-    const storedState = await this.stateRepository.getStateById(stateId);
+    let storedState = await this.stateRepository.getStateById(stateId);
@@
-    if (!storedState) {
-      this.logger.error(eventId, 'last state delta not found');
-      throw new Error(`State at event ${eventId} not found`);
-    }
+    if (!storedState) {
+      // Fallback: event was stored without a state mapping (e.g., rejected/conflicted).
+      const lastWithState =
+        await this.eventRepository.findLatestEventByRoomIdBeforeTimestampWithAssociatedState(
+          event.event.room_id,
+          event.event.origin_server_ts,
+        );
+      const fallbackStateId = lastWithState?.stateId;
+      if (!fallbackStateId) {
+        this.logger.error({ eventId }, 'state mapping not found at/before event');
+        throw new Error(`State at event ${eventId} not found`);
+      }
+      const fallback = await this.stateRepository.getStateById(fallbackStateId);
+      if (!fallback) {
+        this.logger.error({ eventId, fallbackStateId }, 'fallback state missing');
+        throw new Error(`State mapping ${fallbackStateId} not found`);
+      }
+      storedState = fallback;
+    }

Also applies to: 120-131

🤖 Prompt for AI Agents
In packages/federation-sdk/src/services/state.service.ts around lines 146-151
(and similarly 120-131), the current logic throws when a referenced stateId is
missing; change it to handle events persisted without a stateId by falling back
to the latest state mapping at or before the event timestamp: first check if
stateId is present and try stateRepository.getStateById; if that returns null or
if stateId is missing, call a repository method to fetch the latest state
mapping at or before the event's timestamp (e.g.,
getLatestStateByTimestamp/getStateMappingAtOrBefore) and use that state; only
log and throw if both direct lookup and the timestamp-based fallback return
nothing, and include eventId/timestamp in logs for context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants