From 16926a063c4cbe9afa1ed0f375328461dab05413 Mon Sep 17 00:00:00 2001 From: hyrious Date: Thu, 1 Jun 2023 17:34:39 +0800 Subject: [PATCH 1/8] perf(flat-stores): fetch users info only when necessary - only fetch minimal users info once on initialize, which includes the owner and on stage users - only fetch other users info when open users panel --- .../flat-pages/src/components/UsersButton.tsx | 7 +- .../flat-stores/src/classroom-store/index.ts | 28 +++--- packages/flat-stores/src/room-store.ts | 7 -- packages/flat-stores/src/user-store.ts | 98 +++++++++++++++++-- 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/packages/flat-pages/src/components/UsersButton.tsx b/packages/flat-pages/src/components/UsersButton.tsx index 61ebaf297ed..8fd5151ca6d 100644 --- a/packages/flat-pages/src/components/UsersButton.tsx +++ b/packages/flat-pages/src/components/UsersButton.tsx @@ -19,7 +19,12 @@ export const UsersButton = observer(function UsersButton({ cla const users = useComputed(() => { const { offlineJoiners } = classroom; const { speakingJoiners, handRaisingJoiners, otherJoiners } = classroom.users; - return [...speakingJoiners, ...handRaisingJoiners, ...offlineJoiners, ...otherJoiners].sort( + + // speaking users may include offline users, so filter them out + const offlineUserUUIDs = new Set(offlineJoiners.map(user => user.userUUID)); + const speakingOnline = speakingJoiners.filter(user => !offlineUserUUIDs.has(user.userUUID)); + + return [...speakingOnline, ...handRaisingJoiners, ...offlineJoiners, ...otherJoiners].sort( (a, b) => a.name.localeCompare(b.name), ); }).get(); diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index 6150bd2c1c0..6b17757809e 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -352,15 +352,6 @@ export class ClassroomStore { public async init(): Promise { await roomStore.syncOrdinaryRoomInfo(this.roomUUID); - if (process.env.NODE_ENV === "development") { - if (this.roomInfo && this.roomInfo.ownerUUID !== this.ownerUUID) { - (this.ownerUUID as string) = this.roomInfo.ownerUUID; - if (process.env.DEV) { - console.error(new Error("ClassRoom Error: ownerUUID mismatch!")); - } - } - } - await this.initRTC(); await this.rtm.joinRoom({ @@ -372,8 +363,6 @@ export class ClassroomStore { const fastboard = await this.whiteboardStore.joinWhiteboardRoom(); - await this.users.initUsers([this.ownerUUID, ...this.rtm.members]); - const deviceStateStorage = fastboard.syncedStore.connectStorage( "deviceState", {}, @@ -404,6 +393,17 @@ export class ClassroomStore { this.whiteboardStorage = whiteboardStorage; this.userWindowsStorage = userWindowsStorage; + const onStageUsers = Object.keys(onStageUsersStorage.state).filter( + userUUID => onStageUsersStorage.state[userUUID], + ); + await this.users.initUsers([...this.rtm.members], [this.ownerUUID, ...onStageUsers]); + const owner = this.users.cachedUsers.get(this.ownerUUID); + // update owner info in room store, it will use that to render the users panel + roomStore.updateRoom(this.roomUUID, this.ownerUUID, { + ownerName: owner?.name, + ownerAvatarURL: owner?.avatar, + }); + if (this.isCreator) { this.updateDeviceState( this.userUUID, @@ -457,7 +457,7 @@ export class ClassroomStore { this.sideEffect.addDisposer( this.rtm.events.on("member-joined", async ({ userUUID }) => { - await this.users.addUser(userUUID); + await this.users.addUser(userUUID, this.isUsersPanelVisible); this.users.updateUsers(user => { if (user.userUUID === userUUID) { if (userUUID === this.ownerUUID || onStageUsersStorage.state[userUUID]) { @@ -785,6 +785,10 @@ export class ClassroomStore { public toggleUsersPanel = (visible = !this.isUsersPanelVisible): void => { this.isUsersPanelVisible = visible; + // fetch lazy loaded users when the users panel is opened + if (visible) { + this.users.flushLazyUsers().catch(console.error); + } }; public onDragStart = (): void => { diff --git a/packages/flat-stores/src/room-store.ts b/packages/flat-stores/src/room-store.ts index 4dc19545a26..aad3ce5fea9 100644 --- a/packages/flat-stores/src/room-store.ts +++ b/packages/flat-stores/src/room-store.ts @@ -21,7 +21,6 @@ import { recordInfo, RoomStatus, RoomType, - usersInfo, } from "@netless/flat-server-api"; import { globalStore } from "./global-store"; import { preferencesStore } from "./preferences-store"; @@ -133,16 +132,10 @@ export class RoomStore { public async syncOrdinaryRoomInfo(roomUUID: string): Promise { const { roomInfo, ...restInfo } = await ordinaryRoomInfo(roomUUID); - // always include owner avatar url in full room info - const { [roomInfo.ownerUUID]: owner } = await usersInfo({ - roomUUID, - usersUUID: [roomInfo.ownerUUID], - }); this.updateRoom(roomUUID, roomInfo.ownerUUID, { ...restInfo, ...roomInfo, roomUUID, - ownerAvatarURL: owner.avatarURL, }); } diff --git a/packages/flat-stores/src/user-store.ts b/packages/flat-stores/src/user-store.ts index 017f9d41520..b058427ab50 100644 --- a/packages/flat-stores/src/user-store.ts +++ b/packages/flat-stores/src/user-store.ts @@ -1,12 +1,16 @@ import { action, makeAutoObservable, observable } from "mobx"; import { usersInfo, CloudRecordStartPayload } from "@netless/flat-server-api"; import { preferencesStore } from "./preferences-store"; +import { isEmpty } from "lodash-es"; export interface User { + // key userUUID: string; + // static info, can be cached rtcUID: string; avatar: string; name: string; + // state, should keep camera: boolean; mic: boolean; isSpeak: boolean; @@ -74,24 +78,34 @@ export class UserStore { makeAutoObservable(this); } - public initUsers = async (userUUIDs: string[]): Promise => { + // If provided `onStageUserUUIDs`, other users will be lazily initialized. + public initUsers = async (userUUIDs: string[], onStageUserUUIDs?: string[]): Promise => { this.otherJoiners.clear(); this.speakingJoiners.clear(); this.handRaisingJoiners.clear(); - const users = await this.createUsers(userUUIDs); + const users = await this.createUsers(onStageUserUUIDs || userUUIDs); users.forEach(user => { this.sortUser(user); this.cacheUser(user); }); + if (onStageUserUUIDs) { + const lazyUsers = userUUIDs.filter(userUUID => !onStageUserUUIDs.includes(userUUID)); + this.createLazyUsers(lazyUsers); + } }; - public addUser = async (userUUID: string): Promise => { + public addUser = async (userUUID: string, force = false): Promise => { if (this.cachedUsers.has(userUUID)) { this.removeUser(userUUID); } - const [user] = await this.createUsers([userUUID]); - this.cacheUser(user); - this.sortUser(user); + let user: User; + if (force) { + user = (await this.createUsers([userUUID]))[0]; + this.cacheUser(user); + this.sortUser(user); + } else { + user = this.createLazyUsers([userUUID], true)[0]; + } return user; }; @@ -160,8 +174,29 @@ export class UserStore { const users = await this.createUsers( userUUIDs.filter(userUUID => !this.cachedUsers.has(userUUID)), ); + this.cacheUsers(users); + }; + + /** + * Fetch info of lazily initialized users. + */ + public flushLazyUsers = async (userUUIDs?: string[]): Promise => { + if (!userUUIDs) { + userUUIDs = []; + for (const user of this.cachedUsers.values()) { + if (!user.rtcUID) { + userUUIDs.push(user.userUUID); + } + } + } + if (userUUIDs.length === 0) { + return; + } + const users = await this.createUsers(userUUIDs); + this.cacheUsers(users); for (const user of users) { - this.cacheUser(user); + this.removeUser(user.userUUID); + this.sortUser(user); } }; @@ -217,6 +252,22 @@ export class UserStore { this.cachedUsers.set(user.userUUID, user); } + private cacheUsers(users: User[]): void { + for (const user of users) { + // keep user state from cache + const cachedUser = this.cachedUsers.get(user.userUUID); + if (cachedUser) { + user.camera = cachedUser.camera; + user.mic = cachedUser.mic; + user.isSpeak = cachedUser.isSpeak; + user.wbOperate = cachedUser.wbOperate; + user.isRaiseHand = cachedUser.isRaiseHand; + user.hasLeft = !this.isInRoom(user.userUUID); + } + this.cacheUser(user); + } + } + /** * Fetch users info and return an observable user list */ @@ -229,6 +280,10 @@ export class UserStore { // The users info may not include all users in userUUIDs. const users = await usersInfo({ roomUUID: this.roomUUID, usersUUID: userUUIDs }); + if (isEmpty(users)) { + return []; + } + const result: User[] = []; for (const userUUID of userUUIDs) { if (users[userUUID]) { @@ -252,6 +307,35 @@ export class UserStore { return result; } + // Users with empty 'rtcUID' are initialized lazily. + // If 'force' is true, it will reset these users' states. + private createLazyUsers(userUUIDs: string[], force = false): User[] { + const users: User[] = []; + for (const userUUID of userUUIDs) { + const cachedUser = this.cachedUsers.get(userUUID); + if (!force && cachedUser) { + users.push(cachedUser); + continue; + } + const user = observable.object({ + userUUID, + rtcUID: cachedUser?.rtcUID || "", + avatar: cachedUser?.avatar || "", + name: cachedUser?.name || "", + camera: false, + mic: false, + isSpeak: false, + wbOperate: false, + isRaiseHand: false, + hasLeft: !this.isInRoom(userUUID), + }); + users.push(user); + this.sortUser(user); + this.cacheUser(user); + } + return users; + } + private readonly joinerGroups = [ { group: "speakingJoiners", shouldMoveOut: (user: User): boolean => !user.isSpeak }, { group: "handRaisingJoiners", shouldMoveOut: (user: User): boolean => !user.isRaiseHand }, From b1de3022bf9d66fb7f2873f4650e9fc200700320 Mon Sep 17 00:00:00 2001 From: hyrious Date: Thu, 1 Jun 2023 18:48:18 +0800 Subject: [PATCH 2/8] perf(flat-stores): use rtm to broadcast cached user info --- packages/flat-server-api/src/room.ts | 8 +-- .../src/services/text-chat/commands.ts | 7 ++- .../src/services/text-chat/events.ts | 9 ++- .../flat-stores/src/classroom-store/index.ts | 59 ++++++++++++++++++- packages/flat-stores/src/user-store.ts | 14 ++++- service-providers/agora-rtm/src/rtm.ts | 31 +++++++--- 6 files changed, 112 insertions(+), 16 deletions(-) diff --git a/packages/flat-server-api/src/room.ts b/packages/flat-server-api/src/room.ts index 5fd96d75265..2cf85f642b2 100644 --- a/packages/flat-server-api/src/room.ts +++ b/packages/flat-server-api/src/room.ts @@ -376,12 +376,10 @@ export interface UsersInfoPayload { usersUUID?: string[]; } +export type UserInfo = { name: string; rtcUID: number; avatarURL: string }; + export type UsersInfoResult = { - [key in string]: { - name: string; - rtcUID: number; - avatarURL: string; - }; + [key in string]: UserInfo; }; export function usersInfo(payload: UsersInfoPayload): Promise { diff --git a/packages/flat-services/src/services/text-chat/commands.ts b/packages/flat-services/src/services/text-chat/commands.ts index bae76bdc9bd..011de0c0ffc 100644 --- a/packages/flat-services/src/services/text-chat/commands.ts +++ b/packages/flat-services/src/services/text-chat/commands.ts @@ -1,4 +1,4 @@ -import type { RoomStatus } from "@netless/flat-server-api"; +import type { RoomStatus, UserInfo } from "@netless/flat-server-api"; /** From teacher to students */ export interface IServiceTextChatRoomCommandData { @@ -6,6 +6,9 @@ export interface IServiceTextChatRoomCommandData { "ban": { roomUUID: string; status: boolean }; "notice": { roomUUID: string; text: string }; "reward": { roomUUID: string; userUUID: string }; + // Everyone, send this message on join room + // Users that in 'peers' should send back the 'users-info' command + "enter": { roomUUID: string; userUUID: string; userInfo: UserInfo; peers?: string[] }; } export type IServiceTextChatRoomCommandNames = keyof IServiceTextChatRoomCommandData; @@ -25,6 +28,8 @@ export interface IServiceTextChatPeerCommandData { "request-device-response": { roomUUID: string; camera?: boolean; mic?: boolean }; /** From teacher to student */ "notify-device-off": { roomUUID: string; camera?: false; mic?: false }; + /** From everyone to everyone, should send this when received 'enter' command above */ + "users-info": { roomUUID: string; users: Record }; } export type IServiceTextChatPeerCommandNames = keyof IServiceTextChatPeerCommandData; diff --git a/packages/flat-services/src/services/text-chat/events.ts b/packages/flat-services/src/services/text-chat/events.ts index c2699893518..ba06ec17b92 100644 --- a/packages/flat-services/src/services/text-chat/events.ts +++ b/packages/flat-services/src/services/text-chat/events.ts @@ -1,4 +1,4 @@ -import type { RoomStatus } from "@netless/flat-server-api"; +import type { RoomStatus, UserInfo } from "@netless/flat-server-api"; import type { Remitter } from "remitter"; export interface IServiceTextChatEventData { @@ -48,6 +48,13 @@ export interface IServiceTextChatEventData { senderID: string; deviceState: { camera?: boolean; mic?: boolean }; }; + "enter": { + roomUUID: string; + userUUID: string; + userInfo: UserInfo; + peers?: string[]; + }; + "users-info": { roomUUID: string; userUUID: string; users: Record }; } export type IServiceTextChatEventNames = Extract; diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index 6b17757809e..3452b5d2265 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -10,6 +10,7 @@ import { RoomStatus, RoomType, checkRTMCensor, + UserInfo, } from "@netless/flat-server-api"; import { FlatI18n } from "@netless/flat-i18n"; import { errorTips, message } from "flat-components"; @@ -29,6 +30,7 @@ import { IServiceWhiteboard, } from "@netless/flat-services"; import { preferencesStore } from "../preferences-store"; +import { sampleSize } from "lodash-es"; export * from "./constants"; export * from "./chat-store"; @@ -396,7 +398,8 @@ export class ClassroomStore { const onStageUsers = Object.keys(onStageUsersStorage.state).filter( userUUID => onStageUsersStorage.state[userUUID], ); - await this.users.initUsers([...this.rtm.members], [this.ownerUUID, ...onStageUsers]); + const members = [...this.rtm.members]; + await this.users.initUsers(members, [this.ownerUUID, this.userUUID, ...onStageUsers]); const owner = this.users.cachedUsers.get(this.ownerUUID); // update owner info in room store, it will use that to render the users panel roomStore.updateRoom(this.roomUUID, this.ownerUUID, { @@ -404,6 +407,37 @@ export class ClassroomStore { ownerAvatarURL: owner?.avatar, }); + const user = this.users.cachedUsers.get(this.userUUID); + if (user) { + void this.rtm.sendRoomCommand("enter", { + roomUUID: this.roomUUID, + userUUID: user.userUUID, + userInfo: { + name: user.name, + avatarURL: user.avatar, + rtcUID: +user.rtcUID || 0, + }, + peers: sampleSize(members, 3), + }); + } + + this.sideEffect.addDisposer( + this.rtm.events.on("enter", ({ userUUID, userInfo, peers }) => { + this.users.cacheUserIfNeeded(userUUID, userInfo); + if (peers && peers.includes(this.userUUID)) { + this.sendUsersInfoToPeer(userUUID); + } + }), + ); + + this.sideEffect.addDisposer( + this.rtm.events.on("users-info", ({ users }) => { + for (const userUUID in users) { + this.users.cacheUserIfNeeded(userUUID, users[userUUID]); + } + }), + ); + if (this.isCreator) { this.updateDeviceState( this.userUUID, @@ -756,6 +790,29 @@ export class ClassroomStore { } } + private sendUsersInfoToPeer(userUUID: string): void { + const users: Record = {}; + + for (const user of this.users.cachedUsers.values()) { + if (user.rtcUID) { + users[user.userUUID] = { + rtcUID: +user.rtcUID || 0, + name: user.name, + avatarURL: user.avatar, + }; + } + } + + void this.rtm.sendPeerCommand( + "users-info", + { + roomUUID: this.roomUUID, + users, + }, + userUUID, + ); + } + public async destroy(): Promise { this.sideEffect.flushAll(); diff --git a/packages/flat-stores/src/user-store.ts b/packages/flat-stores/src/user-store.ts index b058427ab50..85c05bf8672 100644 --- a/packages/flat-stores/src/user-store.ts +++ b/packages/flat-stores/src/user-store.ts @@ -1,5 +1,5 @@ import { action, makeAutoObservable, observable } from "mobx"; -import { usersInfo, CloudRecordStartPayload } from "@netless/flat-server-api"; +import { usersInfo, CloudRecordStartPayload, UserInfo } from "@netless/flat-server-api"; import { preferencesStore } from "./preferences-store"; import { isEmpty } from "lodash-es"; @@ -109,6 +109,18 @@ export class UserStore { return user; }; + public cacheUserIfNeeded = (userUUID: string, userInfo: UserInfo): void => { + let user = this.cachedUsers.get(userUUID); + if (!user) { + user = this.createLazyUsers([userUUID])[0]; + } + if (!user.rtcUID) { + user.name = userInfo.name; + user.avatar = userInfo.avatarURL; + user.rtcUID = String(userInfo.rtcUID); + } + }; + public removeUser = (userUUID: string): void => { if (this.creator && this.creator.userUUID === userUUID) { this.creator.hasLeft = true; diff --git a/service-providers/agora-rtm/src/rtm.ts b/service-providers/agora-rtm/src/rtm.ts index afd79961c7b..b68c04ac89a 100644 --- a/service-providers/agora-rtm/src/rtm.ts +++ b/service-providers/agora-rtm/src/rtm.ts @@ -231,15 +231,15 @@ export class AgoraRTM extends IServiceTextChat { break; } case RtmEngine.MessageType.RAW: { - if (senderID === ownerUUID) { - try { - const command = JSON.parse( - new TextDecoder().decode(msg.rawMessage), - ) as IServiceTextChatRoomCommand; + try { + const command = JSON.parse( + new TextDecoder().decode(msg.rawMessage), + ) as IServiceTextChatRoomCommand; + if (senderID === ownerUUID || command.t === "enter") { this._emitRoomCommand(roomUUID, senderID, command); - } catch (e) { - console.error(e); } + } catch (e) { + console.error(e); } break; } @@ -292,6 +292,14 @@ export class AgoraRTM extends IServiceTextChat { }); break; } + case "users-info": { + this.events.emit("users-info", { + roomUUID, + userUUID: senderID, + users: command.v.users, + }); + break; + } } } catch (e) { console.error(e); @@ -376,6 +384,15 @@ export class AgoraRTM extends IServiceTextChat { }); break; } + case "enter": { + this.events.emit("enter", { + roomUUID, + userUUID: command.v.userUUID, + userInfo: command.v.userInfo, + peers: command.v.peers, + }); + break; + } } } } From 12dc8a267c3b9c79440c7dcf96b1d2e7f8de9d78 Mon Sep 17 00:00:00 2001 From: hyrious Date: Fri, 2 Jun 2023 09:54:56 +0800 Subject: [PATCH 3/8] refactor a bit - flat-components: add login shortcuts in dev mode - flat-components: lift up windows system buttons - flat-stores: fetch raise hand users info when necessary - flat-stores: ignore self "enter" command --- .../LoginPage/LoginWithPhone/index.tsx | 22 +++++++++ .../WindowsSystemBtn/style.less | 3 +- .../flat-stores/src/classroom-store/index.ts | 17 +++++-- packages/flat-stores/src/user-store.ts | 48 ++++++++++++------- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/packages/flat-components/src/components/LoginPage/LoginWithPhone/index.tsx b/packages/flat-components/src/components/LoginPage/LoginWithPhone/index.tsx index a77a32cfea9..f315de35752 100644 --- a/packages/flat-components/src/components/LoginPage/LoginWithPhone/index.tsx +++ b/packages/flat-components/src/components/LoginPage/LoginWithPhone/index.tsx @@ -229,6 +229,28 @@ export const LoginWithPhone: React.FC = ({
+ {process.env.DEV ? ( + + ) : null} { - this.users.cacheUserIfNeeded(userUUID, userInfo); + this.rtm.events.on("enter", ({ userUUID: senderID, userInfo, peers }) => { + if (senderID === this.userUUID) { + // ignore self enter message + return; + } + this.users.cacheUserIfNeeded(senderID, userInfo); if (peers && peers.includes(this.userUUID)) { - this.sendUsersInfoToPeer(userUUID); + this.sendUsersInfoToPeer(senderID); } }), ); @@ -1133,6 +1137,13 @@ export class ClassroomStore { public onToggleHandRaisingPanel = (force = !this.isHandRaisingPanelVisible): void => { this.isHandRaisingPanelVisible = force; + // fetch lazy loaded users when the hand raising panel is opened + if (force) { + const raiseHandUsers = this.classroomStorage?.state.raiseHandUsers; + if (raiseHandUsers) { + this.users.flushLazyUsers(raiseHandUsers).catch(console.error); + } + } }; public onToggleBan = (): void => { diff --git a/packages/flat-stores/src/user-store.ts b/packages/flat-stores/src/user-store.ts index 85c05bf8672..96ccae3a5e4 100644 --- a/packages/flat-stores/src/user-store.ts +++ b/packages/flat-stores/src/user-store.ts @@ -78,22 +78,29 @@ export class UserStore { makeAutoObservable(this); } - // If provided `onStageUserUUIDs`, other users will be lazily initialized. - public initUsers = async (userUUIDs: string[], onStageUserUUIDs?: string[]): Promise => { + // If provided `forceUserUUIDs`, other users will be lazily initialized. + public initUsers = async (userUUIDs: string[], forceUserUUIDs?: string[]): Promise => { this.otherJoiners.clear(); this.speakingJoiners.clear(); this.handRaisingJoiners.clear(); - const users = await this.createUsers(onStageUserUUIDs || userUUIDs); + const users = await this.createUsers(forceUserUUIDs || userUUIDs); users.forEach(user => { this.sortUser(user); this.cacheUser(user); }); - if (onStageUserUUIDs) { - const lazyUsers = userUUIDs.filter(userUUID => !onStageUserUUIDs.includes(userUUID)); - this.createLazyUsers(lazyUsers); + if (forceUserUUIDs) { + const lazyUsers = userUUIDs.filter(userUUID => !forceUserUUIDs.includes(userUUID)); + this.createLazyUsers(lazyUsers).forEach(user => { + this.sortUser(user); + this.cacheUser(user); + }); } }; + /** + * Add a user, by default it will be lazily initialized. + * If `force` is true, it will be initialized immediately. + */ public addUser = async (userUUID: string, force = false): Promise => { if (this.cachedUsers.has(userUUID)) { this.removeUser(userUUID); @@ -101,11 +108,11 @@ export class UserStore { let user: User; if (force) { user = (await this.createUsers([userUUID]))[0]; - this.cacheUser(user); - this.sortUser(user); } else { user = this.createLazyUsers([userUUID], true)[0]; } + this.cacheUser(user); + this.sortUser(user); return user; }; @@ -113,6 +120,8 @@ export class UserStore { let user = this.cachedUsers.get(userUUID); if (!user) { user = this.createLazyUsers([userUUID])[0]; + this.cacheUser(user); + this.sortUser(user); } if (!user.rtcUID) { user.name = userInfo.name; @@ -319,14 +328,22 @@ export class UserStore { return result; } - // Users with empty 'rtcUID' are initialized lazily. - // If 'force' is true, it will reset these users' states. + /** + * Map users uuid to an observable user list. + * If `force`, it will reset the user state (camera, mic, etc.) + */ private createLazyUsers(userUUIDs: string[], force = false): User[] { - const users: User[] = []; + userUUIDs = [...new Set(userUUIDs)]; + + if (userUUIDs.length <= 0) { + return []; + } + + const result: User[] = []; for (const userUUID of userUUIDs) { const cachedUser = this.cachedUsers.get(userUUID); if (!force && cachedUser) { - users.push(cachedUser); + result.push(cachedUser); continue; } const user = observable.object({ @@ -341,11 +358,10 @@ export class UserStore { isRaiseHand: false, hasLeft: !this.isInRoom(userUUID), }); - users.push(user); - this.sortUser(user); - this.cacheUser(user); + result.push(user); } - return users; + + return result; } private readonly joinerGroups = [ From 922e599c5b1af5312d2972ce4208c72dcfdd2fe2 Mon Sep 17 00:00:00 2001 From: hyrious Date: Fri, 2 Jun 2023 10:06:07 +0800 Subject: [PATCH 4/8] refactor: log rtm cache behavior in dev mode --- .../flat-stores/src/classroom-store/index.ts | 17 +++++++++++++++-- packages/flat-stores/src/user-store.ts | 6 +++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index d8648cdc12b..7772beaf0ea 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -427,17 +427,29 @@ export class ClassroomStore { // ignore self enter message return; } + if (process.env.DEV) { + console.log(`[rtm] ${senderID} is entering room with his info:`, userInfo); + } this.users.cacheUserIfNeeded(senderID, userInfo); if (peers && peers.includes(this.userUUID)) { this.sendUsersInfoToPeer(senderID); + if (process.env.DEV) { + console.log(`[rtm] send local users info to peer ${senderID}`); + } } }), ); this.sideEffect.addDisposer( - this.rtm.events.on("users-info", ({ users }) => { + this.rtm.events.on("users-info", ({ userUUID: senderID, users }) => { + let count = 0; for (const userUUID in users) { - this.users.cacheUserIfNeeded(userUUID, users[userUUID]); + if (this.users.cacheUserIfNeeded(userUUID, users[userUUID])) { + count++; + } + } + if (process.env.DEV) { + console.log(`[rtm] received users info from ${senderID}: %d rows`, count); } }), ); @@ -797,6 +809,7 @@ export class ClassroomStore { private sendUsersInfoToPeer(userUUID: string): void { const users: Record = {}; + // Filter out initialized users (whose rtcUID is not null) for (const user of this.users.cachedUsers.values()) { if (user.rtcUID) { users[user.userUUID] = { diff --git a/packages/flat-stores/src/user-store.ts b/packages/flat-stores/src/user-store.ts index 96ccae3a5e4..9f37c5f351d 100644 --- a/packages/flat-stores/src/user-store.ts +++ b/packages/flat-stores/src/user-store.ts @@ -116,18 +116,22 @@ export class UserStore { return user; }; - public cacheUserIfNeeded = (userUUID: string, userInfo: UserInfo): void => { + // Returns `true` if user info is updated. + public cacheUserIfNeeded = (userUUID: string, userInfo: UserInfo): boolean => { let user = this.cachedUsers.get(userUUID); if (!user) { user = this.createLazyUsers([userUUID])[0]; this.cacheUser(user); this.sortUser(user); } + let updated = false; if (!user.rtcUID) { user.name = userInfo.name; user.avatar = userInfo.avatarURL; user.rtcUID = String(userInfo.rtcUID); + updated = true; } + return updated; }; public removeUser = (userUUID: string): void => { From dbbb3feac79a69ba92155eb0b318a53b86aeb277 Mon Sep 17 00:00:00 2001 From: hyrious Date: Fri, 2 Jun 2023 11:41:52 +0800 Subject: [PATCH 5/8] perf: cache text encoder/decoder --- service-providers/agora-rtm/src/rtm.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/service-providers/agora-rtm/src/rtm.ts b/service-providers/agora-rtm/src/rtm.ts index b68c04ac89a..fc172ed0296 100644 --- a/service-providers/agora-rtm/src/rtm.ts +++ b/service-providers/agora-rtm/src/rtm.ts @@ -16,6 +16,8 @@ import { v4 as uuidv4 } from "uuid"; export class AgoraRTM extends IServiceTextChat { public readonly members = new Set(); + private readonly _encoder = new TextEncoder(); + private readonly _decoder = new TextDecoder(); private readonly _sideEffect = new SideEffectManager(); private readonly _roomSideEffect = new SideEffectManager(); @@ -131,7 +133,7 @@ export class AgoraRTM extends IServiceTextChat { const command = { t, v } as IServiceTextChatRoomCommand; await this.channel.sendMessage({ messageType: RtmEngine.MessageType.RAW, - rawMessage: new TextEncoder().encode(JSON.stringify(command)), + rawMessage: this._encoder.encode(JSON.stringify(command)), }); // emit to local if (this.roomUUID && this.userUUID) { @@ -165,7 +167,7 @@ export class AgoraRTM extends IServiceTextChat { const result = await this.client.sendMessageToPeer( { messageType: RtmEngine.MessageType.RAW, - rawMessage: new TextEncoder().encode(JSON.stringify({ t, v })), + rawMessage: this._encoder.encode(JSON.stringify({ t, v })), }, peerID, ); @@ -233,7 +235,7 @@ export class AgoraRTM extends IServiceTextChat { case RtmEngine.MessageType.RAW: { try { const command = JSON.parse( - new TextDecoder().decode(msg.rawMessage), + this._decoder.decode(msg.rawMessage), ) as IServiceTextChatRoomCommand; if (senderID === ownerUUID || command.t === "enter") { this._emitRoomCommand(roomUUID, senderID, command); @@ -254,7 +256,7 @@ export class AgoraRTM extends IServiceTextChat { if (msg.messageType === RtmEngine.MessageType.RAW) { try { const command = JSON.parse( - new TextDecoder().decode(msg.rawMessage), + this._decoder.decode(msg.rawMessage), ) as IServiceTextChatPeerCommand; if (command.v.roomUUID !== roomUUID) { return; From 12eb553ea807089a4d3cc18e0d6a26cddf26d5f4 Mon Sep 17 00:00:00 2001 From: hyrious Date: Fri, 2 Jun 2023 11:44:43 +0800 Subject: [PATCH 6/8] refactor: disable peer command "users-info" for now --- .../flat-stores/src/classroom-store/index.ts | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index 7772beaf0ea..b13b2e79969 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -806,28 +806,30 @@ export class ClassroomStore { } } - private sendUsersInfoToPeer(userUUID: string): void { - const users: Record = {}; - + // @TODO: use RTM 2.0 and get users info from peer properties + private sendUsersInfoToPeer(_userUUID: string): void { + // @TODO: disabled for now, be cause RTM 1.0 has a limit of 1KB per message + // + // const users: Record = {}; + // // Filter out initialized users (whose rtcUID is not null) - for (const user of this.users.cachedUsers.values()) { - if (user.rtcUID) { - users[user.userUUID] = { - rtcUID: +user.rtcUID || 0, - name: user.name, - avatarURL: user.avatar, - }; - } - } - - void this.rtm.sendPeerCommand( - "users-info", - { - roomUUID: this.roomUUID, - users, - }, - userUUID, - ); + // for (const user of this.users.cachedUsers.values()) { + // if (user.rtcUID) { + // users[user.userUUID] = { + // rtcUID: +user.rtcUID || 0, + // name: user.name, + // avatarURL: user.avatar, + // }; + // } + // } + // void this.rtm.sendPeerCommand( + // "users-info", + // { + // roomUUID: this.roomUUID, + // users, + // }, + // userUUID, + // ); } public async destroy(): Promise { From 4d1beee13d5d650a77e4445589a7fa988ff5c0f7 Mon Sep 17 00:00:00 2001 From: hyrious Date: Fri, 2 Jun 2023 11:48:52 +0800 Subject: [PATCH 7/8] refactor: remove unused type --- packages/flat-stores/src/classroom-store/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index b13b2e79969..dbd477fa4a2 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -10,7 +10,6 @@ import { RoomStatus, RoomType, checkRTMCensor, - UserInfo, } from "@netless/flat-server-api"; import { FlatI18n } from "@netless/flat-i18n"; import { errorTips, message } from "flat-components"; From 743d22f35cfdc92109176f1b95aa8f8f3002dc02 Mon Sep 17 00:00:00 2001 From: hyrious Date: Fri, 2 Jun 2023 12:22:20 +0800 Subject: [PATCH 8/8] refactor: minimize api usage when on stage users change --- packages/flat-stores/src/classroom-store/index.ts | 2 +- packages/flat-stores/src/user-store.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index dbd477fa4a2..28a25be0afb 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -565,7 +565,7 @@ export class ClassroomStore { const onStageUsers = Object.keys(onStageUsersStorage.state).filter( userUUID => onStageUsersStorage.state[userUUID], ); - await this.users.syncExtraUsersInfo(onStageUsers); + await this.users.flushLazyUsers(onStageUsers); runInAction(() => { this.onStageUserUUIDs.replace(onStageUsers); }); diff --git a/packages/flat-stores/src/user-store.ts b/packages/flat-stores/src/user-store.ts index 9f37c5f351d..2e846f21879 100644 --- a/packages/flat-stores/src/user-store.ts +++ b/packages/flat-stores/src/user-store.ts @@ -203,11 +203,18 @@ export class UserStore { }; /** - * Fetch info of lazily initialized users. + * Fetch info of lazily initialized users, or create new one if not exist. */ - public flushLazyUsers = async (userUUIDs?: string[]): Promise => { - if (!userUUIDs) { - userUUIDs = []; + public flushLazyUsers = async (wanted?: string[]): Promise => { + const userUUIDs: string[] = []; + if (wanted) { + for (const userUUID of wanted) { + const user = this.cachedUsers.get(userUUID); + if (!user || !user.rtcUID) { + userUUIDs.push(userUUID); + } + } + } else { for (const user of this.cachedUsers.values()) { if (!user.rtcUID) { userUUIDs.push(user.userUUID);