From fdbe41415240bac9e205e7602063e635cee0c0a3 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 30 Oct 2025 21:30:32 +0530 Subject: [PATCH] Fix circular dependency --- src/modules/StoresApi.ts | 79 +++++++++++++++++------ test/unit-tests/modules/StoresApi-test.ts | 13 +++- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/modules/StoresApi.ts b/src/modules/StoresApi.ts index 2c1341e3b9..f1d6add95e 100644 --- a/src/modules/StoresApi.ts +++ b/src/modules/StoresApi.ts @@ -11,45 +11,86 @@ import { Watchable, } from "@element-hq/element-web-module-api"; -import RoomListStoreV3, { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT } from "../stores/room-list-v3/RoomListStoreV3"; +import type { RoomListStoreV3Class, RoomListStoreV3Event } from "../stores/room-list-v3/RoomListStoreV3"; import { Room as ModuleRoom } from "./models/Room"; -class RoomListStoreApi implements IRoomListStore { +interface RlsEvents { + LISTS_LOADED_EVENT: RoomListStoreV3Event.ListsLoaded; + LISTS_UPDATE_EVENT: RoomListStoreV3Event.ListsUpdate; +} + +export class RoomListStoreApi implements IRoomListStore { + private rls?: RoomListStoreV3Class; + private LISTS_LOADED_EVENT?: RoomListStoreV3Event.ListsLoaded; + private LISTS_UPDATE_EVENT?: RoomListStoreV3Event.ListsUpdate; + public readonly moduleLoadPromise: Promise; + + public constructor() { + this.moduleLoadPromise = this.init(); + } + + /** + * Load the RLS through a dynamic import. This is necessary to prevent + * circular dependency issues. + */ + private async init(): Promise { + const module = await import("../stores/room-list-v3/RoomListStoreV3"); + this.rls = module.default.instance; + this.LISTS_LOADED_EVENT = module.LISTS_LOADED_EVENT; + this.LISTS_UPDATE_EVENT = module.LISTS_UPDATE_EVENT; + } + public getRooms(): RoomsWatchable { - return new RoomsWatchable(); + return new RoomsWatchable(this.roomListStore, this.events); + } + + private get events(): RlsEvents { + if (!this.LISTS_LOADED_EVENT || !this.LISTS_UPDATE_EVENT) { + throw new Error("Event type was not loaded correctly, did you forget to await waitForReady()?"); + } + return { LISTS_LOADED_EVENT: this.LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT: this.LISTS_UPDATE_EVENT }; + } + + private get roomListStore(): RoomListStoreV3Class { + if (!this.rls) { + throw new Error("rls is undefined, did you forget to await waitForReady()?"); + } + return this.rls; } public async waitForReady(): Promise { - // Check if RLS is already loaded - if (!RoomListStoreV3.instance.isLoadingRooms) return; + // Wait for the module to load first + await this.moduleLoadPromise; - // Return a promise that resolves when RLS has loaded - let resolve: () => void; - const promise: Promise = new Promise((_resolve) => { - resolve = _resolve; - }); - RoomListStoreV3.instance.once(LISTS_LOADED_EVENT, () => { - resolve(); - }); - return promise; + // Check if RLS is already loaded + if (!this.roomListStore.isLoadingRooms) return; + + // Await a promise that resolves when RLS has loaded + const { promise, resolve } = Promise.withResolvers(); + const { LISTS_LOADED_EVENT } = this.events; + this.roomListStore.once(LISTS_LOADED_EVENT, resolve); + await promise; } } class RoomsWatchable extends Watchable { - public constructor() { - super(RoomListStoreV3.instance.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom))); + public constructor( + private readonly rls: RoomListStoreV3Class, + private readonly events: RlsEvents, + ) { + super(rls.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom))); } private onRlsUpdate = (): void => { - this.value = RoomListStoreV3.instance.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom)); + this.value = this.rls.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom)); }; protected onFirstWatch(): void { - RoomListStoreV3.instance.on(LISTS_UPDATE_EVENT, this.onRlsUpdate); + this.rls.on(this.events.LISTS_UPDATE_EVENT, this.onRlsUpdate); } protected onLastWatch(): void { - RoomListStoreV3.instance.off(LISTS_UPDATE_EVENT, this.onRlsUpdate); + this.rls.off(this.events.LISTS_UPDATE_EVENT, this.onRlsUpdate); } } diff --git a/test/unit-tests/modules/StoresApi-test.ts b/test/unit-tests/modules/StoresApi-test.ts index 10d0cd6078..ba8a0c83da 100644 --- a/test/unit-tests/modules/StoresApi-test.ts +++ b/test/unit-tests/modules/StoresApi-test.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { waitFor } from "jest-matrix-react"; -import { StoresApi } from "../../../src/modules/StoresApi"; +import { type RoomListStoreApi, StoresApi } from "../../../src/modules/StoresApi"; import RoomListStoreV3, { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT, @@ -30,6 +30,9 @@ describe("StoresApi", () => { })(); // Shouldn't have resolved yet. expect(hasResolved).toStrictEqual(false); + + // Wait for the module to load so that we can test the listener. + await (store.roomListStore as RoomListStoreApi).moduleLoadPromise; // Emit the loaded event. RoomListStoreV3.instance.emit(LISTS_LOADED_EVENT); // Should resolve now. @@ -39,28 +42,32 @@ describe("StoresApi", () => { }); describe("getRooms()", () => { - it("should return rooms from RLS", () => { + it("should return rooms from RLS", async () => { const cli = stubClient(); const room1 = mkRoom(cli, "!foo1:m.org"); const room2 = mkRoom(cli, "!foo2:m.org"); const room3 = mkRoom(cli, "!foo3:m.org"); jest.spyOn(RoomListStoreV3.instance, "getSortedRooms").mockReturnValue([room1, room2, room3]); + jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); const store = new StoresApi(); + await store.roomListStore.waitForReady(); const watchable = store.roomListStore.getRooms(); expect(watchable.value).toHaveLength(3); expect(watchable.value[0]).toBeInstanceOf(Room); }); - it("should update from RLS", () => { + it("should update from RLS", async () => { const cli = stubClient(); const room1 = mkRoom(cli, "!foo1:m.org"); const room2 = mkRoom(cli, "!foo2:m.org"); const rooms = [room1, room2]; jest.spyOn(RoomListStoreV3.instance, "getSortedRooms").mockReturnValue(rooms); + jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); const store = new StoresApi(); + await store.roomListStore.waitForReady(); const watchable = store.roomListStore.getRooms(); const fn = jest.fn(); watchable.watch(fn);