Fix circular dependency

This commit is contained in:
R Midhun Suresh 2025-10-30 21:30:32 +05:30
parent 353609c05d
commit fdbe414152
No known key found for this signature in database
2 changed files with 70 additions and 22 deletions

View File

@ -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<void>;
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<void> {
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<void> {
// 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<void> = 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<void>();
const { LISTS_LOADED_EVENT } = this.events;
this.roomListStore.once(LISTS_LOADED_EVENT, resolve);
await promise;
}
}
class RoomsWatchable extends Watchable<Room[]> {
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);
}
}

View File

@ -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);