From d137242b3fd36a895973b416b9c9bf6acf024096 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 14 Mar 2025 22:12:55 +0700 Subject: [PATCH 01/22] Improve boot time by implementing sliding sync --- .../app/components/ai-assistant/panel.gts | 3 +- packages/host/app/services/matrix-service.ts | 216 +++++++++++++++--- packages/host/ember-cli-build.js | 4 +- .../matrix/docker/synapse/dev/homeserver.yaml | 3 + packages/matrix/docker/synapse/index.ts | 2 +- patches/matrix-js-sdk@31.0.0.patch | Bin 4654 -> 7347 bytes pnpm-lock.yaml | 12 +- 7 files changed, 204 insertions(+), 36 deletions(-) diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index f5e7fe0d28..98af6af612 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -657,7 +657,8 @@ export default class AiAssistantPanel extends Component { return Boolean( this.matrixService.currentRoomId && this.maybeMonacoSDK && - this.doCreateRoom.isIdle, + this.doCreateRoom.isIdle && + !this.matrixService.isLoadingTimeline, ); } } diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index fd2ae855a6..4c12abb0f5 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -4,7 +4,7 @@ import { debounce } from '@ember/runloop'; import Service, { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; -import { task } from 'ember-concurrency'; +import { task, timeout, restartableTask } from 'ember-concurrency'; import window from 'ember-window-mock'; import { cloneDeep } from 'lodash'; import { @@ -14,6 +14,12 @@ import { type EmittedEvents, type ISendEventResponse, } from 'matrix-js-sdk'; +import { + SlidingSync, + MSC3575List, + SlidingSyncEvent, +} from 'matrix-js-sdk/lib/sliding-sync'; +import { SlidingSyncState } from 'matrix-js-sdk/lib/sliding-sync'; import stringify from 'safe-stable-stringify'; import { md5 } from 'super-fast-md5'; import { TrackedMap } from 'tracked-built-ins'; @@ -115,6 +121,10 @@ import type * as MatrixSDK from 'matrix-js-sdk'; const { matrixURL } = ENV; const MAX_CARD_SIZE_KB = 60; const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name']; +const SLIDING_SYNC_AI_ROOM_LIST_NAME = 'ai-room'; +const SLIDING_SYNC_AUTH_ROOM_LIST_NAME = 'auth-room'; +const SLIDING_SYNC_LIST_RANGE_SIZE = 2; +const SLIDING_SYNC_LIST_TIMELINE_LIMIT = 1; const realmEventsLogger = logger('realm:events'); @@ -139,6 +149,12 @@ export default class MatrixService extends Service { @tracked private _isNewUser = false; @tracked private postLoginCompleted = false; @tracked private _currentRoomId: string | undefined; + @tracked private timelineLoadingState: Map = + new TrackedMap(); + @tracked private currentAiRoomListRange = SLIDING_SYNC_LIST_RANGE_SIZE; + @tracked private totalAiRooms?: number; + @tracked private totalAuthRooms?: number; + @tracked private currentAuthRoomListRange = SLIDING_SYNC_LIST_RANGE_SIZE; profile = getMatrixProfile(this, () => this.userId); @@ -164,6 +180,7 @@ export default class MatrixService extends Service { new TrackedMap(); private cardHashes: Map = new Map(); // hashes <> event id private skillCardHashes: Map = new Map(); // hashes <> event id + private slidingSync: SlidingSync | undefined; constructor(owner: Owner) { super(owner); @@ -181,6 +198,7 @@ export default class MatrixService extends Service { set currentRoomId(value: string | undefined) { this._currentRoomId = value; if (value) { + this.loadAllTimelineEvents(value); window.localStorage.setItem(CurrentRoomIdPersistenceKey, value); } else { window.localStorage.removeItem(CurrentRoomIdPersistenceKey); @@ -475,8 +493,9 @@ export default class MatrixService extends Service { this.bindEventListeners(); try { - await this._client.startClient(); - let accountDataContent = await this._client.getAccountDataFromServer<{ + this.initSlidingSync(); + await this.client.startClient({ slidingSync: this.slidingSync }); + let accountDataContent = await this.client.getAccountDataFromServer<{ realms: string[]; }>(APP_BOXEL_REALMS_EVENT_TYPE); await this.realmServer.setAvailableRealmURLs( @@ -498,6 +517,126 @@ export default class MatrixService extends Service { } } + private initSlidingSync() { + let lists: Map = new Map(); + lists.set(SLIDING_SYNC_AI_ROOM_LIST_NAME, { + ranges: [[0, SLIDING_SYNC_LIST_RANGE_SIZE]], + filters: { + is_dm: false, + }, + timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT, + required_state: [['*', '*']], + }); + lists.set(SLIDING_SYNC_AUTH_ROOM_LIST_NAME, { + ranges: [[0, SLIDING_SYNC_LIST_RANGE_SIZE]], + filters: { + is_dm: true, + }, + timeline_limit: 1, + required_state: [['*', '*']], + }); + this.slidingSync = new SlidingSync( + this.client.baseUrl, + lists, + { + timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT, + }, + this.client as any, + 3000, + ); + + this.slidingSync.on(SlidingSyncEvent.Lifecycle, (state, response) => { + if (state !== SlidingSyncState.Complete || !response) { + return; + } + + // Handle AI rooms + if ( + response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME]?.ops?.[0].op === 'SYNC' + ) { + if ( + response.lists?.[SLIDING_SYNC_AI_ROOM_LIST_NAME]?.count !== undefined + ) { + this.totalAiRooms = + response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].count; + const currentRange = + response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].ops?.[0]?.range; + + if (currentRange) { + this.currentAiRoomListRange = currentRange[1]; + this.maybeLoadMoreAiRooms.perform(); + } + } + } + + // Handle Auth rooms + if ( + response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME]?.ops?.[0].op === 'SYNC' + ) { + if ( + response.lists?.[SLIDING_SYNC_AUTH_ROOM_LIST_NAME]?.count !== + undefined + ) { + this.totalAuthRooms = + response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME].count; + const currentAuthRange = + response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME].ops?.[0]?.range; + + if (currentAuthRange) { + this.currentAuthRoomListRange = currentAuthRange[1]; + this.maybeLoadMoreAuthRooms.perform(); + } + } + } + }); + + return this.slidingSync; + } + + private maybeLoadMoreAiRooms = restartableTask(async () => { + if (!this.totalAiRooms || !this.slidingSync) { + return; + } + + while (this.currentAiRoomListRange < this.totalAiRooms) { + const nextRange = Math.min( + this.currentAiRoomListRange + 2, + this.totalAiRooms, + ); + + try { + await this.slidingSync.setListRanges('ai-room', [[0, nextRange]]); + this.currentAiRoomListRange = nextRange; + await timeout(500); + } catch (error) { + console.error('Error expanding room range:', error); + break; + } + } + }); + + private maybeLoadMoreAuthRooms = restartableTask(async () => { + if (!this.totalAuthRooms || !this.slidingSync) { + return; + } + + while (this.currentAuthRoomListRange < this.totalAuthRooms) { + const nextRange = Math.min( + this.currentAuthRoomListRange + 2, + this.totalAuthRooms, + ); + + try { + await this.slidingSync.setListRanges('auth-room', [[0, nextRange]]); + this.currentAuthRoomListRange = nextRange; + await timeout(500); + } catch (error) { + console.error('Error expanding auth room range:', error); + break; + } + } + }); + private async loginToRealms() { // This is where we would actually load user-specific choices out of the // user's profile based on this.client.getUserId(); @@ -534,7 +673,7 @@ export default class MatrixService extends Service { | CommandResultWithOutputContent | CommandDefinitionsContent, ) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); return roomData.mutex.dispatch(async () => { if ('data' in content) { const encodedContent = { @@ -1008,7 +1147,7 @@ export default class MatrixService extends Service { } async setPowerLevel(roomId: string, userId: string, powerLevel: number) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { return this.client.setPowerLevel(roomId, userId, powerLevel); }); @@ -1045,7 +1184,7 @@ export default class MatrixService extends Service { content: Record, stateKey: string = '', ) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { return this.client.sendStateEvent(roomId, eventType, content, stateKey); }); @@ -1059,7 +1198,7 @@ export default class MatrixService extends Service { content: Record, ) => Promise>, ) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { let currentContent = await this.getStateEventSafe( roomId, @@ -1077,21 +1216,21 @@ export default class MatrixService extends Service { } async leave(roomId: string) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { return this.client.leave(roomId); }); } async forget(roomId: string) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { return this.client.forget(roomId); }); } async setRoomName(roomId: string, name: string) { - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { return this.client.setRoomName(roomId, name); }); @@ -1131,11 +1270,43 @@ export default class MatrixService extends Service { return await this.client.isUsernameAvailable(username); } - async getRoomState(roomId: string) { - return this.client - .getRoom(roomId) - ?.getLiveTimeline() - .getState('f' as MatrixSDK.Direction); + private async loadAllTimelineEvents(roomId: string) { + let roomData = this.ensureRoomData(roomId); + let room = this.client.getRoom(roomId); + + if (!room) { + throw new Error(`Cannot find room with id ${roomId}`); + } + + this.timelineLoadingState.set(roomId, true); + try { + while (room.oldState.paginationToken != null) { + await this.client.scrollback(room); + await timeout(1000); + let rs = room.getLiveTimeline().getState('f' as MatrixSDK.Direction); + if (rs) { + roomData.notifyRoomStateUpdated(rs); + } + } + + const timeline = room.getLiveTimeline(); + const events = timeline.getEvents(); + for (let event of events) { + await this.processDecryptedEvent(this.buildEventForProcessing(event)); + } + } catch (error) { + console.error('Error loading timeline events:', error); + throw error; + } finally { + this.timelineLoadingState.set(roomId, false); + } + } + + get isLoadingTimeline() { + if (!this.currentRoomId) { + return false; + } + return this.timelineLoadingState.get(this.currentRoomId) ?? false; } async sendActiveLLMEvent(roomId: string, model: string) { @@ -1152,18 +1323,14 @@ export default class MatrixService extends Service { `bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`, ); } - let roomData = await this.ensureRoomData(roomId); + let roomData = this.ensureRoomData(roomId); roomData.addEvent(event, oldEventId); } - private async ensureRoomData(roomId: string) { + private ensureRoomData(roomId: string) { let roomData = this.getRoomData(roomId); if (!roomData) { roomData = new Room(roomId); - let rs = await this.getRoomState(roomId); - if (rs) { - roomData.notifyRoomStateUpdated(rs); - } this.setRoomData(roomId, roomData); } return roomData; @@ -1282,7 +1449,7 @@ export default class MatrixService extends Service { } roomStates = Array.from(roomStateMap.values()); for (let rs of roomStates) { - let roomData = await this.ensureRoomData(rs.roomId); + let roomData = this.ensureRoomData(rs.roomId); roomData.notifyRoomStateUpdated(rs); } roomStateUpdatesDrained!(); @@ -1482,11 +1649,6 @@ export default class MatrixService extends Service { ) { this.commandService.executeCommandEventsIfNeeded(event); } - - if (room.oldState.paginationToken != null) { - // we need to scroll back to capture any room events fired before this one - await this.client?.scrollback(room); - } } async activateCodingSkill() { diff --git a/packages/host/ember-cli-build.js b/packages/host/ember-cli-build.js index a3443bb163..9eaf8a6b65 100644 --- a/packages/host/ember-cli-build.js +++ b/packages/host/ember-cli-build.js @@ -86,7 +86,9 @@ module.exports = function (defaults) { process: false, }, alias: { - 'matrix-js-sdk': 'matrix-js-sdk/src/browser-index.ts', // Consume matrix-js-sdk via Typescript ESM so that code splitting works to exlcude massive matrix-sdk-crypto-wasm from the main bundle + 'matrix-js-sdk$': 'matrix-js-sdk/src/browser-index.ts', // Consume matrix-js-sdk via Typescript ESM so that code splitting works to exlcude massive matrix-sdk-crypto-wasm from the main bundle + 'matrix-js-sdk/src/sliding-sync': + 'matrix-js-sdk/src/sliding-sync.ts', }, }, node: { diff --git a/packages/matrix/docker/synapse/dev/homeserver.yaml b/packages/matrix/docker/synapse/dev/homeserver.yaml index 5c798c7b96..8db86fbcc4 100644 --- a/packages/matrix/docker/synapse/dev/homeserver.yaml +++ b/packages/matrix/docker/synapse/dev/homeserver.yaml @@ -99,3 +99,6 @@ email: templates: custom_template_directory: "/custom/templates/" + +experimental_features: + msc3575_enabled: true diff --git a/packages/matrix/docker/synapse/index.ts b/packages/matrix/docker/synapse/index.ts index b312778d15..b64c824828 100644 --- a/packages/matrix/docker/synapse/index.ts +++ b/packages/matrix/docker/synapse/index.ts @@ -126,7 +126,7 @@ export async function synapseStart( ); await dockerCreateNetwork({ networkName: 'boxel' }); const synapseId = await dockerRun({ - image: 'matrixdotorg/synapse:v1.99.0rc1', + image: 'matrixdotorg/synapse:v1.126.0', containerName: 'boxel-synapse', dockerParams: [ '--rm', diff --git a/patches/matrix-js-sdk@31.0.0.patch b/patches/matrix-js-sdk@31.0.0.patch index 2ca992c3748635497e2f684274778cae2213ba00..ffbad81a402dbf3596f98b30f6382f2621cf426a 100644 GIT binary patch delta 2659 zcmb_dO>f*p7~Zs$ZWazXP>BPFQQ?qkyYu@*rXCD9Y)jM4BB#zNwaFQrQhUVAf z$ynuuKyT8;?Q-Szz+h;^-IcK zClm}vZV*|KhiPvx7@#2+Z-?1DO?&-*zm?4HE*e;dh59zy-R<=|ozBMIoZFqw7A(8) z>3Ksh?igC!R+S5@2aS0{2az+H<`ksw|Ne{47TOO#Ezkczk#!jlJF-)B7(H*7kS0=D z7F4tG;NeG)K7QPN-2J5cx*VV8=VL|GT<($LmEIlac}kaSLVrla`DE*}{XA17$wW~( zBwT>WPd@?*^7FlXo^e_Ej%!7Cv%5Lj`uyt3b3Ft3R&;HkE9_VX^4#G1db9iF%i;5* z$(wZgB+DVCy7~=hd^LK1{{E;;6P{!z19_P-^JfVdNE14gZRrAqUThNv zG;#vlwkVDsbr!eSqLeh>z}9g-;IK1cSXhPehK zx%226n0F`7W&sAZ5> zpneUJQu9P89dR%JjOqS*5#|^GluW7i&XQbwG|*v5j}X zJo@d^Hap3J4OyCqOqozFfT6ixKmoWUw3z4mj^zUqyucO_^{^v=eHdqf?Kxo($ILV% zEHI8Zs4ePJKa4#di-0ko_#ur5hpkt-k?k%SuZBYV&>947nZAA;gjnDIVg`}8nB|39 z;NXy`BDvVFY5|QB@P5$obzA=JDmvl@k`O3h3%SEqs>iL<$oYUgmg@A{$NK=hH2GXS%A zxqT?qY0k~>a;XgTbf};3Qv;pNpajSYsGv7kK?D}FLd40%2rMVlsxgaO9?na68-%7} zm=`Bztuv>R>4TpdO-SAsfT0dQlo))jIz%5%OsN@3U#PbQ@nER1ng80}Dc}#G` z1P(3YI)phmuxuKbrb~FheBX^d3YOS*Xe2ld2o^r(jwc*1@@$~M+Yql4Di{0VDdvaJ3Anl9Uo*UkSQ#Hro&STRyXpd)l7SYGfQ z36C>7G0@?-tT3*%FK^Oo9RaObx=FL6CDUuyigQP(j=JRDrF(O}&8y`-EWGx-?YDHh o$;V5d7B4S_t8Qq19zpf{?(WSOUcG(Yire?0!=(C0)FAQRKXClh_W%F@ delta 14 VcmdmNxlU#C1g6ETn~S-+c>pb~1#=18.0.0'} dependencies: From f9145a9ce7bb239ed97f15e449e3b85bd59f9de2 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Mon, 17 Mar 2025 21:46:43 +0700 Subject: [PATCH 02/22] Fix host tests --- .../ai-assistant/attachment-picker/index.gts | 7 +- .../app/components/ai-assistant/panel.gts | 3 +- packages/host/app/components/matrix/room.gts | 28 +++- packages/host/app/services/matrix-service.ts | 122 +----------------- .../host/tests/helpers/mock-matrix/_client.ts | 4 + .../host/tests/helpers/mock-matrix/_sdk.ts | 8 ++ 6 files changed, 47 insertions(+), 125 deletions(-) diff --git a/packages/host/app/components/ai-assistant/attachment-picker/index.gts b/packages/host/app/components/ai-assistant/attachment-picker/index.gts index eb2d59e3d0..7911833282 100644 --- a/packages/host/app/components/ai-assistant/attachment-picker/index.gts +++ b/packages/host/app/components/ai-assistant/attachment-picker/index.gts @@ -8,7 +8,7 @@ import { restartableTask } from 'ember-concurrency'; import { TrackedSet } from 'tracked-built-ins'; import { AddButton, Tooltip, Pill } from '@cardstack/boxel-ui/components'; -import { and, cn, eq, gt, not } from '@cardstack/boxel-ui/helpers'; +import { and, cn, eq, gt, not, or } from '@cardstack/boxel-ui/helpers'; import { chooseCard, @@ -39,6 +39,7 @@ interface Signature { submode: Submode; maxNumberOfItemsToAttach?: number; autoAttachedCardTooltipMessage?: string; + disabled?: boolean; }; } @@ -119,7 +120,7 @@ export default class AiAssistantAttachmentPicker extends Component { @iconWidth='14' @iconHeight='14' {{on 'click' this.chooseFile}} - @disabled={{this.doChooseFile.isRunning}} + @disabled={{(or this.doChooseFile.isRunning @disabled)}} data-test-choose-file-btn > @@ -133,7 +134,7 @@ export default class AiAssistantAttachmentPicker extends Component { @iconWidth='14' @iconHeight='14' {{on 'click' this.chooseCard}} - @disabled={{this.doChooseCard.isRunning}} + @disabled={{(or this.doChooseCard.isRunning @disabled)}} data-test-choose-card-btn > diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index 98af6af612..f5e7fe0d28 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -657,8 +657,7 @@ export default class AiAssistantPanel extends Component { return Boolean( this.matrixService.currentRoomId && this.maybeMonacoSDK && - this.doCreateRoom.isIdle && - !this.matrixService.isLoadingTimeline, + this.doCreateRoom.isIdle, ); } } diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index d09f68d63a..3a6a4a6e61 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -27,8 +27,8 @@ import { TrackedObject, TrackedSet } from 'tracked-built-ins'; import { v4 as uuidv4 } from 'uuid'; -import { BoxelButton } from '@cardstack/boxel-ui/components'; -import { eq, not } from '@cardstack/boxel-ui/helpers'; +import { BoxelButton, LoadingIndicator } from '@cardstack/boxel-ui/components'; +import { eq, not, or } from '@cardstack/boxel-ui/helpers'; import { ResolvedCodeRef, internalKeyFor } from '@cardstack/runtime-common'; import { DEFAULT_LLM_LIST } from '@cardstack/runtime-common/matrix-constants'; @@ -104,7 +104,14 @@ export default class Room extends Component { data-test-message-idx={{i}} /> {{else}} - + {{#if this.matrixService.isLoadingTimeline}} + + {{else}} + + {{/if}} {{/each}} {{#if this.room}} {{#if this.showUnreadIndicator}} @@ -154,12 +161,16 @@ export default class Room extends Component { 'Current card is shared automatically' 'Topmost card is shared automatically' }} + @disabled={{this.matrixService.isLoadingTimeline}} /> @@ -213,6 +224,12 @@ export default class Room extends Component { :deep(.ai-assistant-conversation > *:nth-last-of-type(2)) { padding-bottom: var(--boxel-sp-xl); } + .loading-indicator { + margin-top: auto; + margin-bottom: auto; + margin-left: auto; + margin-right: auto; + } @@ -798,7 +815,8 @@ export default class Room extends Component { this.autoAttachedCards.size !== 0, ) && !!this.room && - !this.messages.some((m) => this.isPendingMessage(m)) + !this.messages.some((m) => this.isPendingMessage(m)) && + !this.matrixService.isLoadingTimeline ); } diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 4c12abb0f5..e40d161b58 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -4,7 +4,7 @@ import { debounce } from '@ember/runloop'; import Service, { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; -import { task, timeout, restartableTask } from 'ember-concurrency'; +import { restartableTask, task } from 'ember-concurrency'; import window from 'ember-window-mock'; import { cloneDeep } from 'lodash'; import { @@ -14,12 +14,7 @@ import { type EmittedEvents, type ISendEventResponse, } from 'matrix-js-sdk'; -import { - SlidingSync, - MSC3575List, - SlidingSyncEvent, -} from 'matrix-js-sdk/lib/sliding-sync'; -import { SlidingSyncState } from 'matrix-js-sdk/lib/sliding-sync'; +import { SlidingSync, MSC3575List } from 'matrix-js-sdk/lib/sliding-sync'; import stringify from 'safe-stable-stringify'; import { md5 } from 'super-fast-md5'; import { TrackedMap } from 'tracked-built-ins'; @@ -123,7 +118,7 @@ const MAX_CARD_SIZE_KB = 60; const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name']; const SLIDING_SYNC_AI_ROOM_LIST_NAME = 'ai-room'; const SLIDING_SYNC_AUTH_ROOM_LIST_NAME = 'auth-room'; -const SLIDING_SYNC_LIST_RANGE_SIZE = 2; +const SLIDING_SYNC_LIST_RANGE_SIZE = 10; const SLIDING_SYNC_LIST_TIMELINE_LIMIT = 1; const realmEventsLogger = logger('realm:events'); @@ -151,10 +146,6 @@ export default class MatrixService extends Service { @tracked private _currentRoomId: string | undefined; @tracked private timelineLoadingState: Map = new TrackedMap(); - @tracked private currentAiRoomListRange = SLIDING_SYNC_LIST_RANGE_SIZE; - @tracked private totalAiRooms?: number; - @tracked private totalAuthRooms?: number; - @tracked private currentAuthRoomListRange = SLIDING_SYNC_LIST_RANGE_SIZE; profile = getMatrixProfile(this, () => this.userId); @@ -198,7 +189,7 @@ export default class MatrixService extends Service { set currentRoomId(value: string | undefined) { this._currentRoomId = value; if (value) { - this.loadAllTimelineEvents(value); + this.loadAllTimelineEvents.perform(value); window.localStorage.setItem(CurrentRoomIdPersistenceKey, value); } else { window.localStorage.removeItem(CurrentRoomIdPersistenceKey); @@ -545,98 +536,9 @@ export default class MatrixService extends Service { 3000, ); - this.slidingSync.on(SlidingSyncEvent.Lifecycle, (state, response) => { - if (state !== SlidingSyncState.Complete || !response) { - return; - } - - // Handle AI rooms - if ( - response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME]?.ops?.[0].op === 'SYNC' - ) { - if ( - response.lists?.[SLIDING_SYNC_AI_ROOM_LIST_NAME]?.count !== undefined - ) { - this.totalAiRooms = - response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].count; - const currentRange = - response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].ops?.[0]?.range; - - if (currentRange) { - this.currentAiRoomListRange = currentRange[1]; - this.maybeLoadMoreAiRooms.perform(); - } - } - } - - // Handle Auth rooms - if ( - response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME]?.ops?.[0].op === 'SYNC' - ) { - if ( - response.lists?.[SLIDING_SYNC_AUTH_ROOM_LIST_NAME]?.count !== - undefined - ) { - this.totalAuthRooms = - response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME].count; - const currentAuthRange = - response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME].ops?.[0]?.range; - - if (currentAuthRange) { - this.currentAuthRoomListRange = currentAuthRange[1]; - this.maybeLoadMoreAuthRooms.perform(); - } - } - } - }); - return this.slidingSync; } - private maybeLoadMoreAiRooms = restartableTask(async () => { - if (!this.totalAiRooms || !this.slidingSync) { - return; - } - - while (this.currentAiRoomListRange < this.totalAiRooms) { - const nextRange = Math.min( - this.currentAiRoomListRange + 2, - this.totalAiRooms, - ); - - try { - await this.slidingSync.setListRanges('ai-room', [[0, nextRange]]); - this.currentAiRoomListRange = nextRange; - await timeout(500); - } catch (error) { - console.error('Error expanding room range:', error); - break; - } - } - }); - - private maybeLoadMoreAuthRooms = restartableTask(async () => { - if (!this.totalAuthRooms || !this.slidingSync) { - return; - } - - while (this.currentAuthRoomListRange < this.totalAuthRooms) { - const nextRange = Math.min( - this.currentAuthRoomListRange + 2, - this.totalAuthRooms, - ); - - try { - await this.slidingSync.setListRanges('auth-room', [[0, nextRange]]); - this.currentAuthRoomListRange = nextRange; - await timeout(500); - } catch (error) { - console.error('Error expanding auth room range:', error); - break; - } - } - }); - private async loginToRealms() { // This is where we would actually load user-specific choices out of the // user's profile based on this.client.getUserId(); @@ -1270,7 +1172,7 @@ export default class MatrixService extends Service { return await this.client.isUsernameAvailable(username); } - private async loadAllTimelineEvents(roomId: string) { + private loadAllTimelineEvents = restartableTask(async (roomId: string) => { let roomData = this.ensureRoomData(roomId); let room = this.client.getRoom(roomId); @@ -1282,25 +1184,15 @@ export default class MatrixService extends Service { try { while (room.oldState.paginationToken != null) { await this.client.scrollback(room); - await timeout(1000); let rs = room.getLiveTimeline().getState('f' as MatrixSDK.Direction); if (rs) { roomData.notifyRoomStateUpdated(rs); } } - - const timeline = room.getLiveTimeline(); - const events = timeline.getEvents(); - for (let event of events) { - await this.processDecryptedEvent(this.buildEventForProcessing(event)); - } - } catch (error) { - console.error('Error loading timeline events:', error); - throw error; } finally { this.timelineLoadingState.set(roomId, false); } - } + }); get isLoadingTimeline() { if (!this.currentRoomId) { @@ -1575,7 +1467,7 @@ export default class MatrixService extends Service { return; } - let roomData = await this.getRoomData(roomId); + let roomData = this.getRoomData(roomId); // patch in any missing room events--this will support dealing with local // echoes, migrating older histories as well as handle any matrix syncing gaps // that might occur diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 868f69069e..8193d48adb 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -399,6 +399,10 @@ export class MockClient implements ExtendedClient { return { getState: (_direction: MatrixSDK.Direction) => this.serverState.getRoomStateUpdatePayload(roomId!), + getEvents: () => + this.serverState + .getRoomEvents(roomId!) + .map((e) => new MatrixEvent(e)), }; }, } as MatrixSDK.Room; diff --git a/packages/host/tests/helpers/mock-matrix/_sdk.ts b/packages/host/tests/helpers/mock-matrix/_sdk.ts index 05b0332b0d..62b8e6ba63 100644 --- a/packages/host/tests/helpers/mock-matrix/_sdk.ts +++ b/packages/host/tests/helpers/mock-matrix/_sdk.ts @@ -1,3 +1,5 @@ +import { MatrixEvent as MatrixEventClass } from 'matrix-js-sdk'; + import type { ExtendedMatrixSDK } from '@cardstack/host/services/matrix-sdk-loader'; import { MockClient } from './_client'; @@ -57,4 +59,10 @@ export class MockSDK implements PublicAPI { BeaconLiveness: 'RoomState.BeaconLiveness', Marker: 'RoomState.Marker', } as ExtendedMatrixSDK['RoomStateEvent']; + + MatrixEvent = class MatrixEvent { + constructor(event: any) { + return new MatrixEventClass(event); + } + } as ExtendedMatrixSDK['MatrixEvent']; } From f764fe57980c201c35208ac2ef08be638f99d421 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Mon, 17 Mar 2025 21:49:52 +0700 Subject: [PATCH 03/22] revert mock MatrixEvent --- packages/host/tests/helpers/mock-matrix/_sdk.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/host/tests/helpers/mock-matrix/_sdk.ts b/packages/host/tests/helpers/mock-matrix/_sdk.ts index 62b8e6ba63..05b0332b0d 100644 --- a/packages/host/tests/helpers/mock-matrix/_sdk.ts +++ b/packages/host/tests/helpers/mock-matrix/_sdk.ts @@ -1,5 +1,3 @@ -import { MatrixEvent as MatrixEventClass } from 'matrix-js-sdk'; - import type { ExtendedMatrixSDK } from '@cardstack/host/services/matrix-sdk-loader'; import { MockClient } from './_client'; @@ -59,10 +57,4 @@ export class MockSDK implements PublicAPI { BeaconLiveness: 'RoomState.BeaconLiveness', Marker: 'RoomState.Marker', } as ExtendedMatrixSDK['RoomStateEvent']; - - MatrixEvent = class MatrixEvent { - constructor(event: any) { - return new MatrixEventClass(event); - } - } as ExtendedMatrixSDK['MatrixEvent']; } From d881f31b6386bb717c29cea1fa4c26195a7b4a18 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Mon, 17 Mar 2025 21:53:20 +0700 Subject: [PATCH 04/22] wait until all events consumed --- packages/host/app/services/matrix-service.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index e40d161b58..846e9138a0 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -1189,6 +1189,24 @@ export default class MatrixService extends Service { roomData.notifyRoomStateUpdated(rs); } } + + let timeline = room.getLiveTimeline(); + let events = timeline.getEvents(); + await new Promise((resolve) => { + let checkEvents = () => { + let allEventsConsumed = events.every((event) => + roomData.events.some((e) => e.event_id === event.getId()), + ); + + if (allEventsConsumed) { + resolve(); + } else { + setTimeout(checkEvents, 100); + } + }; + + checkEvents(); + }); } finally { this.timelineLoadingState.set(roomId, false); } From 843d928d6d5771c0230002c5a4375359c6b83d13 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Mon, 17 Mar 2025 22:33:00 +0700 Subject: [PATCH 05/22] Fix host tests --- packages/host/app/services/matrix-service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 846e9138a0..48f8e3f7ed 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -2,6 +2,7 @@ import type Owner from '@ember/owner'; import type RouterService from '@ember/routing/router-service'; import { debounce } from '@ember/runloop'; import Service, { service } from '@ember/service'; +import { buildWaiter } from '@ember/test-waiters'; import { cached, tracked } from '@glimmer/tracking'; import { restartableTask, task } from 'ember-concurrency'; @@ -122,6 +123,7 @@ const SLIDING_SYNC_LIST_RANGE_SIZE = 10; const SLIDING_SYNC_LIST_TIMELINE_LIMIT = 1; const realmEventsLogger = logger('realm:events'); +const waiter = buildWaiter('matrix-service:waiter'); export type OperatorModeContext = { submode: Submode; @@ -1181,6 +1183,7 @@ export default class MatrixService extends Service { } this.timelineLoadingState.set(roomId, true); + let token = waiter.beginAsync(); try { while (room.oldState.paginationToken != null) { await this.client.scrollback(room); @@ -1209,6 +1212,7 @@ export default class MatrixService extends Service { }); } finally { this.timelineLoadingState.set(roomId, false); + waiter.endAsync(token); } }); From 17e3a7c789ca07dab40afab910278fb13f75f58b Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Mar 2025 07:18:54 +0700 Subject: [PATCH 06/22] set m.direct --- packages/realm-server/tests/auth-client-test.ts | 6 ++++++ packages/runtime-common/realm-auth-client.ts | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/realm-server/tests/auth-client-test.ts b/packages/realm-server/tests/auth-client-test.ts index 730fe2a6a2..edc6b17b2a 100644 --- a/packages/realm-server/tests/auth-client-test.ts +++ b/packages/realm-server/tests/auth-client-test.ts @@ -37,6 +37,12 @@ module(basename(__filename), function () { async hashMessageWithSecret(_message: string): Promise { throw new Error('Method not implemented.'); }, + async getAccountData() { + return Promise.resolve({}); + }, + async setAccountData() { + return Promise.resolve(); + }, } as RealmAuthMatrixClientInterface; let virtualNetwork = new VirtualNetwork(); diff --git a/packages/runtime-common/realm-auth-client.ts b/packages/runtime-common/realm-auth-client.ts index b3899ebdae..070ac080f5 100644 --- a/packages/runtime-common/realm-auth-client.ts +++ b/packages/runtime-common/realm-auth-client.ts @@ -14,6 +14,8 @@ export interface RealmAuthMatrixClientInterface { joinRoom(room: string): Promise; sendEvent(room: string, type: string, content: any): Promise; hashMessageWithSecret(message: string): Promise; + getAccountData(type: string): Promise; + setAccountData(type: string, data: any): Promise; } interface Options { @@ -101,6 +103,14 @@ export class RealmAuthClient { await this.matrixClient.joinRoom(room); } + let directRooms = await this.matrixClient.getAccountData('m.direct'); + if (!directRooms?.includes(room)) { + let userId = this.matrixClient.getUserId() as string; + await this.matrixClient.setAccountData('m.direct', { + [userId]: [...(directRooms?.[userId] ?? []), room], + }); + } + await this.matrixClient.sendEvent(room, 'm.room.message', { body: `auth-response: ${challenge}`, msgtype: 'm.text', From 06438a4af054d794f94773f5c57363fbc838df29 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Mar 2025 08:27:12 +0700 Subject: [PATCH 07/22] Another sdk patch --- packages/host/app/services/matrix-service.ts | 6 +++--- packages/host/ember-cli-build.js | 2 -- packages/runtime-common/realm-auth-client.ts | 4 ++-- patches/matrix-js-sdk@31.0.0.patch | Bin 7347 -> 18205 bytes pnpm-lock.yaml | 12 ++++++------ 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 48f8e3f7ed..a5b3a29b5d 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -119,7 +119,7 @@ const MAX_CARD_SIZE_KB = 60; const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name']; const SLIDING_SYNC_AI_ROOM_LIST_NAME = 'ai-room'; const SLIDING_SYNC_AUTH_ROOM_LIST_NAME = 'auth-room'; -const SLIDING_SYNC_LIST_RANGE_SIZE = 10; +const SLIDING_SYNC_LIST_RANGE_END = 9; const SLIDING_SYNC_LIST_TIMELINE_LIMIT = 1; const realmEventsLogger = logger('realm:events'); @@ -513,7 +513,7 @@ export default class MatrixService extends Service { private initSlidingSync() { let lists: Map = new Map(); lists.set(SLIDING_SYNC_AI_ROOM_LIST_NAME, { - ranges: [[0, SLIDING_SYNC_LIST_RANGE_SIZE]], + ranges: [[0, SLIDING_SYNC_LIST_RANGE_END]], filters: { is_dm: false, }, @@ -521,7 +521,7 @@ export default class MatrixService extends Service { required_state: [['*', '*']], }); lists.set(SLIDING_SYNC_AUTH_ROOM_LIST_NAME, { - ranges: [[0, SLIDING_SYNC_LIST_RANGE_SIZE]], + ranges: [[0, SLIDING_SYNC_LIST_RANGE_END]], filters: { is_dm: true, }, diff --git a/packages/host/ember-cli-build.js b/packages/host/ember-cli-build.js index 9eaf8a6b65..857c975c13 100644 --- a/packages/host/ember-cli-build.js +++ b/packages/host/ember-cli-build.js @@ -87,8 +87,6 @@ module.exports = function (defaults) { }, alias: { 'matrix-js-sdk$': 'matrix-js-sdk/src/browser-index.ts', // Consume matrix-js-sdk via Typescript ESM so that code splitting works to exlcude massive matrix-sdk-crypto-wasm from the main bundle - 'matrix-js-sdk/src/sliding-sync': - 'matrix-js-sdk/src/sliding-sync.ts', }, }, node: { diff --git a/packages/runtime-common/realm-auth-client.ts b/packages/runtime-common/realm-auth-client.ts index 070ac080f5..4857e17ddc 100644 --- a/packages/runtime-common/realm-auth-client.ts +++ b/packages/runtime-common/realm-auth-client.ts @@ -104,8 +104,8 @@ export class RealmAuthClient { } let directRooms = await this.matrixClient.getAccountData('m.direct'); - if (!directRooms?.includes(room)) { - let userId = this.matrixClient.getUserId() as string; + let userId = this.matrixClient.getUserId() as string; + if (!directRooms?.[userId]?.includes(room)) { await this.matrixClient.setAccountData('m.direct', { [userId]: [...(directRooms?.[userId] ?? []), room], }); diff --git a/patches/matrix-js-sdk@31.0.0.patch b/patches/matrix-js-sdk@31.0.0.patch index ffbad81a402dbf3596f98b30f6382f2621cf426a..d50b0e362ea60b4ed823963e9e3905ef6ac901a0 100644 GIT binary patch literal 18205 zcmeHPX>;60dKTACR_0c!{2{4IzO|UD)yN!Ta38ef=;BtivDPY4&VI1Bm+a@dDqb`59e)fXjD!~oFNl3B@`djwfiJjlJU_HtJB@i7 zCqWhj-tNJ{0h^`b)vR0=#qR$8{t7Jj@3RAQYO;Nc-M_!Pf8)lDorlZRx^d%EJl@1F zwLA2%<%hbr4*g0`rgn$+Xclo%#rXC=|NUR_LXZB^8-o6$8_kXSMiQT|-k`{v)--XM zonL+Q@!$OIuW$bP=5KEP>G-TVKW=%uY##FZ8~XO5s*1RK=JCJh;{4>(zkgJfEze6) z%LREVFy-4nf&;4aht;x78@cva4cz?X<|ikg{?nT`R^<9q=6lxVl)Z^;bIM%Dzr6h9 z=0E)N?DOX*FN=9u0V0Lf9iUzQ{GD#QM$ORIdUafvMOB_+u$$k_yKb*BaE*a$3|wR2 zuNni|EZqFfUo{r4FMo}JYYc3|0O<#YUuy5`R=Jqyp-%cBN1ED?sihx0Q(QqU)9roT zY7cgY#_+SF7Nb>*cXYt9qc%W4)AR1@wsV0y4|qqOj-B-BT`;pFAwWNq3vcY)SMBxZ zH3qIRu!@0?)YnhR3pj$vYz_KXmt23l#=tcO{-0rB=i|r6k00Vv$~bS{j%+Q%?|Z<#1l zR%Nn~v73&I9z`C{lKNuNRx?@DFurS0a@f+VYaVgrdr6phaT>Cr1uwny{4ZAERoSnpwPb{YL# zswSz*re(Eg*geM2Im$%sS>6~iZ^VNI9+r!u;_1JMi{`obekq!EQmgi1m0sNWTy2tP zY?AQ;rG@0>@0+T;gF_DFDQdTF>`@_jxm@6=Mzk;Txu}-yWO9ey`vMF0)q+9*1Ps;N zXQ@f}Oa*3&t@~@JXsWzSRm>lYS9v0gtg25%`&E(tPD6`}O}bkEC#Ywb>@)R&LYRpm z4HjLIunQKG&>73J78$#D?;hLBmSxiBRk=s^*7I>RT|!NxBXs{|_aGtLM2uXeFQ7>@ zBFk`-Ij>(5RUS0YmNCjdXCmz{Qj2z3mqgUhH$~yBZ5Ic8kX4$S7NFC-`cDjU0}L_~<-ym-v$RT@*&jtbd+_YZ>_J@B?Qwou z@?u6L0^{zoj_#7w=uzX7c|&{v=2NnT&$h0LLUJ_KTIF6;b5p%UHVDghiFg-j7!EKv zfvNcb%nV2*<6XhLx4TQ+?wNO!aU~;at{R=zNPK` z9yAwaa?qqN2R_^~qB#-z)rrH*4}-uoEglAum1Ue;vE>SZz9>o(C-XwXFfA`ktb}J) zm|1QT2Vg>B`%Y*%ag=1-jM6lTB5#uux1B!l;XOV1VFqo-iIoc3vY1r^&Whv>;4#-}gi;~BMNDo@477Pl# z8NvQ#edJJTHc7o6Q$5jF2q$})KJuGAsT}wIgtKANG~tI)O#(SiG#zusFl%iOY0T7> zzS-N{Vaz#d);ki_aw6sEIPNsqB1#XE1TQ4CkHH5&m9SOQ{!&Crq6s)2i&Rfq;&Hr6 zqtFqCThRgM4odWwhY8XPX;t2Cl^Wpe_s3uTfmQK$B59@lZI*FEnxG`tQ)+}}%Fcye zR0@%@wlb7Xdk-D;qX@NaXKFcY-?nT*EO5N0HlB+_7;=SZ)U!CBMjl5)~|NhLq1!QcLtk=BXwebRpWs_A+3Sh-B( zCj6v;k5H^^5xp(;v;xv{kv<}Q3ezCX(8hpN+e)g`Qb_BUSWVI58%0qy+AnV?#yaB3 z*VdQ%Ak-$o+ivl%tY$4V<=s|n5cY9Yg@-laT0qgO*GlEC${ zEMahHOvXnv!iH&Kc9bhez(N3fs~(f}m*J%QW$ z0>{eh+tD<{^=0#x>r1YXien|Mku2*PN7I4u#weZ9?|;b>>3~fHU9aaV#Yi%)0{G)l zyrf-@>bk12q<533zouh%1qDZRvr9G>w zwndyo8%wrV-zQQ;j2MYXDN%Q(B>R1uiI_geba?5KKdzEJB_ivLIXaer)Lb@V$u}KS zs@RW0KuKn4d)I|rvm9OsUY-`!>10n{r>q#gq>Frp)(3~MM~FkqQ)6$?!99I}krI9% z0^#RU97Ot9WTGZIlQdht?Gi~W-?68D_!gP~O8nd12R)xv#gNE>D?iVX;yKUTGdLAh z@k-PU%h22Bcut}14ZQz;wv3IWn$Ly(ozF(HLx<3gD}X2t0cT6*2Ijl0S#rkP(?u(> z*_`oO%>2M{MUtfl2@KjnXqwSM9LM-DE!!8V<)l8}-R})WCms{(f#Z*~v$iRO4`jC6 zN6>J+AW)}Hx;;q=7l@A~Z+EjF)TiW;Z1g{Zx`p~$A3_d9zz&SIB8(`>&85}+9pJ($ zW#1-r_88S80BJ~znOYAVE(ar z5d+{9wS`owJ^USN>2e*uCx&5gIuar$xlVyvR_B<6SXvl(?A65xAX=nvw+b663Ji>Q z%bPC+r<|T9om?QP2pJ)TrY4g}txWBK8m>BCKXlrU_v0aJi<1Yca>4H2|a+ zpb|Ef<&6VIE7(ELX5HcezSVVi0Y3ZT2S~?ww=c$AOM0@S(YpXowFH$!ev*#v4<1B; zq$X%#)<#n_4d|GD9nR_bod$4sYTk+(Cg593=j2wu$LQ?$0KZ=j5VmKn6(HkzE1<5x zIPhOb2;7Bq(Foh!NKap_yE>o^t->UpI(g7@dV#ubR^H4}4Uzd&`DcwFp zRmxC9#=J@B+`VZh?`oI#+M~(mi$aF~C&P9fG_s}o@7Hy4NY26Qi-+pl$l0e!MTKBe zqx&i1=@neB&6R$tstc`!+r&9rWJ2@ymJ!Y6LU7;r{1l~0*LO|R$s9*;+mE>$nZB8& zJQYF^mwPZH_-kSKTj0{Eo41V;=Tk`n#dj&=kPgF3{Y&IRnRd2Hx*j{sJziw;INP75+-?Ty->OFnt@sP#% z%Rm(8?;NflD5GTd!w^W0xmQLF{VrrqMtPns`N&~do2w0wh7#0!!ql^`#>yx1AP<$x* zeW+sAx_rL3f|FHOjpDV-{dpYVSa_)cpuTb3?;Sr`*P9M_~{KpkAc32uCjLfehfFK zVVNtH4(W||4?s}a5;}86wAc^2!gck)f z9)$^fZ<9MVPwdDyt=KmVho?S#b0>3S%#18M4n-RK9RHt4l-jPaUA#_+LhEJ>x_`+Z)IyfwZiCplJe@KfgHrAE3v9bmk?pqW&l*=|4?DZAErA3;#v9XbvshOF9F;L9d$UHUK(!$ux!ob4BGHK(4 m=18.0.0'} dependencies: From 1d34114b8689b2588f73eb9b0ac08f7db465df4f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Mar 2025 08:55:07 +0700 Subject: [PATCH 08/22] lint fix --- .../host/app/services/matrix-sdk-loader.ts | 2 +- packages/host/tests/helpers/mock-matrix.ts | 1 + .../host/tests/helpers/mock-matrix/_client.ts | 20 +++++++++++++++++-- .../realm-server/tests/auth-client-test.ts | 2 +- packages/runtime-common/realm-auth-client.ts | 4 ++-- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/host/app/services/matrix-sdk-loader.ts b/packages/host/app/services/matrix-sdk-loader.ts index cef0c65f81..d14a8f7e0e 100644 --- a/packages/host/app/services/matrix-sdk-loader.ts +++ b/packages/host/app/services/matrix-sdk-loader.ts @@ -70,6 +70,7 @@ export type ExtendedClient = Pick< | 'fetchRoomEvent' | 'forget' | 'getAccessToken' + | 'getAccountData' | 'getJoinedRooms' | 'getProfileInfo' | 'getRoom' @@ -92,7 +93,6 @@ export type ExtendedClient = Pick< | 'sendEvent' | 'sendReadReceipt' | 'sendStateEvent' - | 'setAccountData' | 'setDisplayName' | 'setPassword' | 'setPowerLevel' diff --git a/packages/host/tests/helpers/mock-matrix.ts b/packages/host/tests/helpers/mock-matrix.ts index 10c1d028c4..12ed97b176 100644 --- a/packages/host/tests/helpers/mock-matrix.ts +++ b/packages/host/tests/helpers/mock-matrix.ts @@ -16,6 +16,7 @@ export interface Config { expiresInSec?: number; autostart?: boolean; now?: () => number; + directRooms?: Record; } export function setupMockMatrix( diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 8193d48adb..bb2f291cf0 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -121,6 +121,8 @@ export class MockClient implements ExtendedClient { setAccountData(type: string, data: T): Promise<{}> { if (type === APP_BOXEL_REALMS_EVENT_TYPE) { this.sdkOpts.activeRealms = (data as any).realms; + } else if (type === 'm.direct') { + this.sdkOpts.directRooms = data as any; } else { throw new Error( 'Support for updating this event type in account data is not yet implemented in this mock.', @@ -129,8 +131,22 @@ export class MockClient implements ExtendedClient { return Promise.resolve({}); } - getAccountData(_type: string): Promise { - throw new Error('Method not implemented.'); + getAccountData(eventType: string): MatrixEvent | undefined { + if (eventType === 'm.direct') { + return new MatrixEvent({ + type: 'm.direct', + content: this.sdkOpts.directRooms || {}, + }); + } + if (eventType === APP_BOXEL_REALMS_EVENT_TYPE) { + return new MatrixEvent({ + type: APP_BOXEL_REALMS_EVENT_TYPE, + content: { + realms: this.sdkOpts.activeRealms || [], + }, + }); + } + return undefined; } addThreePidOnly(_data: MatrixSDK.IAddThreePidOnlyBody): Promise<{}> { diff --git a/packages/realm-server/tests/auth-client-test.ts b/packages/realm-server/tests/auth-client-test.ts index edc6b17b2a..ff2c6e410c 100644 --- a/packages/realm-server/tests/auth-client-test.ts +++ b/packages/realm-server/tests/auth-client-test.ts @@ -38,7 +38,7 @@ module(basename(__filename), function () { throw new Error('Method not implemented.'); }, async getAccountData() { - return Promise.resolve({}); + return {}; }, async setAccountData() { return Promise.resolve(); diff --git a/packages/runtime-common/realm-auth-client.ts b/packages/runtime-common/realm-auth-client.ts index 4857e17ddc..8e232703a9 100644 --- a/packages/runtime-common/realm-auth-client.ts +++ b/packages/runtime-common/realm-auth-client.ts @@ -14,7 +14,7 @@ export interface RealmAuthMatrixClientInterface { joinRoom(room: string): Promise; sendEvent(room: string, type: string, content: any): Promise; hashMessageWithSecret(message: string): Promise; - getAccountData(type: string): Promise; + getAccountData(type: string): any; setAccountData(type: string, data: any): Promise; } @@ -103,7 +103,7 @@ export class RealmAuthClient { await this.matrixClient.joinRoom(room); } - let directRooms = await this.matrixClient.getAccountData('m.direct'); + let directRooms = this.matrixClient.getAccountData('m.direct'); let userId = this.matrixClient.getUserId() as string; if (!directRooms?.[userId]?.includes(room)) { await this.matrixClient.setAccountData('m.direct', { From c1401d427a4bba4722f85fee6cb1d44eb9613269 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Mar 2025 14:15:38 +0700 Subject: [PATCH 09/22] Load more rooms if needed --- .../ai-assistant/past-session-item.gts | 7 +- .../components/ai-assistant/past-sessions.gts | 143 ++++++++++++------ packages/host/app/services/matrix-service.ts | 112 ++++++++++++-- patches/matrix-js-sdk@31.0.0.patch | Bin 18205 -> 20333 bytes pnpm-lock.yaml | 12 +- 5 files changed, 210 insertions(+), 64 deletions(-) diff --git a/packages/host/app/components/ai-assistant/past-session-item.gts b/packages/host/app/components/ai-assistant/past-session-item.gts index 31337a5de7..0047679c03 100644 --- a/packages/host/app/components/ai-assistant/past-session-item.gts +++ b/packages/host/app/components/ai-assistant/past-session-item.gts @@ -209,7 +209,12 @@ export default class PastSessionItem extends Component { if (!this.args.session.lastMessage) { return false; } - return !this.args.session.lastMessage.isStreamingFinished; + + return ( + this.args.session.lastMessage.author.userId !== + this.matrixService.userId && + !this.args.session.lastMessage.isStreamingFinished + ); } get hasUnseenMessage() { diff --git a/packages/host/app/components/ai-assistant/past-sessions.gts b/packages/host/app/components/ai-assistant/past-sessions.gts index 3ed96cd8d3..d9bd3c4169 100644 --- a/packages/host/app/components/ai-assistant/past-sessions.gts +++ b/packages/host/app/components/ai-assistant/past-sessions.gts @@ -1,13 +1,18 @@ -import type { TemplateOnlyComponent } from '@ember/component/template-only'; import { on } from '@ember/modifier'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; -import { IconButton } from '@cardstack/boxel-ui/components'; +import { modifier } from 'ember-modifier'; + +import { IconButton, LoadingIndicator } from '@cardstack/boxel-ui/components'; import { DropdownArrowFilled } from '@cardstack/boxel-ui/icons'; import { SessionRoomData } from './panel'; import AiAssistantPanelPopover from './panel-popover'; import PastSessionItem, { type RoomActions } from './past-session-item'; +import type MatrixService from '../../services/matrix-service'; + interface Signature { Args: { sessions: SessionRoomData[]; @@ -17,51 +22,93 @@ interface Signature { Element: HTMLElement; } -const AiAssistantPastSessionsList: TemplateOnlyComponent = ; + checkScrollPosition(); -export default AiAssistantPastSessionsList; + element.addEventListener('scroll', checkScrollPosition); + + return () => { + element.removeEventListener('scroll', checkScrollPosition); + }; + }); + + +} diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index a5b3a29b5d..c541ca4f12 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -15,7 +15,13 @@ import { type EmittedEvents, type ISendEventResponse, } from 'matrix-js-sdk'; -import { SlidingSync, MSC3575List } from 'matrix-js-sdk/lib/sliding-sync'; +import { + SlidingSync, + MSC3575List, + SlidingSyncEvent, + SlidingSyncState, + MSC3575SlidingSyncResponse, +} from 'matrix-js-sdk/lib/sliding-sync'; import stringify from 'safe-stable-stringify'; import { md5 } from 'super-fast-md5'; import { TrackedMap } from 'tracked-built-ins'; @@ -114,7 +120,7 @@ import type ResetService from './reset'; import type * as MatrixSDK from 'matrix-js-sdk'; -const { matrixURL } = ENV; +const { matrixURL, environment } = ENV; const MAX_CARD_SIZE_KB = 60; const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name']; const SLIDING_SYNC_AI_ROOM_LIST_NAME = 'ai-room'; @@ -173,7 +179,10 @@ export default class MatrixService extends Service { new TrackedMap(); private cardHashes: Map = new Map(); // hashes <> event id private skillCardHashes: Map = new Map(); // hashes <> event id + private slidingSync: SlidingSync | undefined; + private aiRoomIds: Set = new Set(); + @tracked private isLoadingMoreAIRooms = false; constructor(owner: Owner) { super(owner); @@ -191,7 +200,7 @@ export default class MatrixService extends Service { set currentRoomId(value: string | undefined) { this._currentRoomId = value; if (value) { - this.loadAllTimelineEvents.perform(value); + this.loadAllTimelineEvents(value); window.localStorage.setItem(CurrentRoomIdPersistenceKey, value); } else { window.localStorage.removeItem(CurrentRoomIdPersistenceKey); @@ -243,6 +252,7 @@ export default class MatrixService extends Service { e.event.content.realms, ); await this.loginToRealms(); + await this.maybeLoadMoreAuthRooms(e.event.content.realms); } }, ], @@ -486,7 +496,7 @@ export default class MatrixService extends Service { this.bindEventListeners(); try { - this.initSlidingSync(); + await this.initSlidingSync(); await this.client.startClient({ slidingSync: this.slidingSync }); let accountDataContent = await this.client.getAccountDataFromServer<{ realms: string[]; @@ -510,7 +520,11 @@ export default class MatrixService extends Service { } } - private initSlidingSync() { + private async initSlidingSync() { + let accountData = await this.client.getAccountDataFromServer<{ + realms: string[]; + }>(APP_BOXEL_REALMS_EVENT_TYPE); + let lists: Map = new Map(); lists.set(SLIDING_SYNC_AI_ROOM_LIST_NAME, { ranges: [[0, SLIDING_SYNC_LIST_RANGE_END]], @@ -521,7 +535,14 @@ export default class MatrixService extends Service { required_state: [['*', '*']], }); lists.set(SLIDING_SYNC_AUTH_ROOM_LIST_NAME, { - ranges: [[0, SLIDING_SYNC_LIST_RANGE_END]], + ranges: [ + [ + 0, + accountData + ? accountData?.realms.length + : SLIDING_SYNC_LIST_RANGE_END, + ], + ], filters: { is_dm: true, }, @@ -535,7 +556,25 @@ export default class MatrixService extends Service { timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT, }, this.client as any, - 3000, + environment === 'test' ? 200 : 30000, + ); + this.slidingSync.on( + SlidingSyncEvent.Lifecycle, + ( + state: SlidingSyncState | null, + resp: MSC3575SlidingSyncResponse | null, + ) => { + if ( + state === SlidingSyncState.Complete && + resp && + resp.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].ops?.[0]?.op === 'SYNC' + ) { + for (let roomId of resp.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].ops[0] + .room_ids) { + this.aiRoomIds.add(roomId); + } + } + }, ); return this.slidingSync; @@ -1174,7 +1213,7 @@ export default class MatrixService extends Service { return await this.client.isUsernameAvailable(username); } - private loadAllTimelineEvents = restartableTask(async (roomId: string) => { + private async loadAllTimelineEvents(roomId: string) { let roomData = this.ensureRoomData(roomId); let room = this.client.getRoom(roomId); @@ -1214,7 +1253,7 @@ export default class MatrixService extends Service { this.timelineLoadingState.set(roomId, false); waiter.endAsync(token); } - }); + } get isLoadingTimeline() { if (!this.currentRoomId) { @@ -1597,6 +1636,61 @@ export default class MatrixService extends Service { await roomResource.loading; roomResource.activateLLM(model); } + + loadMoreAIRooms() { + this.loadMoreAIRoomsTask.perform(); + } + + private loadMoreAIRoomsTask = restartableTask(async () => { + if (!this.slidingSync) return; + + let currentList = this.slidingSync.getListParams( + SLIDING_SYNC_AI_ROOM_LIST_NAME, + ); + if (!currentList) return; + + let currentRange = currentList.ranges[0]; + if (!currentRange) return; + + if (this.aiRoomIds.size < currentRange[1] - 1) { + return; + } + + let newEndRange = currentRange[1] + 10; + + this.isLoadingMoreAIRooms = true; + try { + await this.slidingSync.setListRanges(SLIDING_SYNC_AI_ROOM_LIST_NAME, [ + [0, newEndRange], + ]); + } finally { + this.isLoadingMoreAIRooms = false; + } + }); + + get loadingMoreAIRooms() { + return this.isLoadingMoreAIRooms; + } + + async maybeLoadMoreAuthRooms(realms: string[]) { + if (!this.slidingSync) return; + + let currentList = this.slidingSync.getListParams( + SLIDING_SYNC_AUTH_ROOM_LIST_NAME, + ); + if (!currentList) return; + + let currentRange = currentList.ranges[0]; + if (!currentRange) return; + if (realms.length - 1 <= currentRange[1]) { + return; + } + + let newEndRange = realms.length - 1; + await this.slidingSync.setListRanges(SLIDING_SYNC_AUTH_ROOM_LIST_NAME, [ + [0, newEndRange], + ]); + } } function saveAuth(auth: LoginResponse) { diff --git a/patches/matrix-js-sdk@31.0.0.patch b/patches/matrix-js-sdk@31.0.0.patch index d50b0e362ea60b4ed823963e9e3905ef6ac901a0..c197326609aa67693e13ba3a155ada221ff6fa21 100644 GIT binary patch delta 1822 zcmchX&uD8Gad=^$FNQEaoj_Uw;Fl~58FrJxceq!$jbo|$z9CNpMt zk|v^-54|Fku;=#DKf&iBaj(>#dgIImL~mS~*foXZBuRG>)dnjX(eR zr)(TMS}K)}lvs(D+UdW>Bg08ixpg9Y-&&116TG85jKN>V)E9%lCr(eOEQ4p|H%HRv zr{*RMWm4wS0GzIVX}VFv@!L(?hd6{z1aYGYJrqS=(+ZGFkc*&&3G~woUrnV?rY}uG zx9Wmf7rN=w=^sFt>~I7U8OG9_0S_xc|KvJN>S0^Nav}Zo!liz;$%q7KGmZ(D%OaVp z%<5geDYy9&p35VDUZ;!z^J11kl6ZsiP!c@KT;%42;ADj)gk$g>h(eZ-B0-)Kiywtt z>tYpqgC^(XzSGsKpys;Gs;63T;o#|X$2gwt1y0sh&`@nH_j`FcxTXM4&Z0N%`S)YyC0bs0_EPGdVO?hX9JQyZK zj4D;@38UJus&2$A3ajP?bz`x;Gan{oEoQ}5em#QASTIK7dO|xqWHWOWtwzQ3EG?1e zIS1v)N+26lnydTAwYK#6N@`gJOg*oYoqSQhCnzU)L2Pnmy4k)*kgSJ^KFoaBD|})h zHkLRh+qcAuUg8;5MxjnmEo=Dz<*mdd5p0XxibI}+NK(NUNkrmAb)0mIdr&dibQRO` z!H`Vx;aQo?>d~uU8bzR1>re^8D~z_U4BVOf=glUv?Z^%-NO0gmNRSP2Wcf~{_(2x5 z{3vKQ6wm2wFqOVo{CdLD6XiFo^knPqu}rsdCWam66T45ww~D4%T_gAfY${PAt>37h zF=F2DHyWyskaTyy^B6@&y35(RV`#zVsH6* zBktOi6?)uoxVJ57pU!S)=Z)yfBp8a%tu zqXi@;RO}2#jCp=?xk=FS;W-iH`?I?;JKY!MyPY32+C>V8x(6SUv#RqZMNe*6u(5;G ZfdMsy^Y6tpzg{0p^WQPrl-rxF`VSx|M3Ddh diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb66766bd3..9422ee14f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ patchedDependencies: hash: sbmabuxrk2m44agmppz3qozwvq path: patches/magic-string@0.25.9.patch matrix-js-sdk@31.0.0: - hash: pdac54mkqfy2dpqeb2s5y4pvny + hash: xllgzidppgfy66ho7r7op3c7wa path: patches/matrix-js-sdk@31.0.0.patch style-loader@2.0.0: hash: xqjji5denmqrswdovljl2t3yv4 @@ -145,7 +145,7 @@ importers: version: 4.17.21 matrix-js-sdk: specifier: ^31.0.0 - version: 31.0.0(patch_hash=pdac54mkqfy2dpqeb2s5y4pvny) + version: 31.0.0(patch_hash=xllgzidppgfy66ho7r7op3c7wa) openai: specifier: 4.86.1 version: 4.86.1 @@ -240,7 +240,7 @@ importers: version: 6.6.2 matrix-js-sdk: specifier: ^31.0.0 - version: 31.0.0(patch_hash=pdac54mkqfy2dpqeb2s5y4pvny) + version: 31.0.0(patch_hash=xllgzidppgfy66ho7r7op3c7wa) super-fast-md5: specifier: ^1.0.1 version: 1.0.1 @@ -1615,7 +1615,7 @@ importers: version: 1.8.1 matrix-js-sdk: specifier: ^31.0.0 - version: 31.0.0(patch_hash=pdac54mkqfy2dpqeb2s5y4pvny) + version: 31.0.0(patch_hash=xllgzidppgfy66ho7r7op3c7wa) moment: specifier: ^2.29.4 version: 2.29.4 @@ -2199,7 +2199,7 @@ importers: dependencies: matrix-js-sdk: specifier: ^31.0.0 - version: 31.0.0(patch_hash=pdac54mkqfy2dpqeb2s5y4pvny) + version: 31.0.0(patch_hash=xllgzidppgfy66ho7r7op3c7wa) devDependencies: '@babel/preset-typescript': specifier: ^7.24.7 @@ -19697,7 +19697,7 @@ packages: /matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - /matrix-js-sdk@31.0.0(patch_hash=pdac54mkqfy2dpqeb2s5y4pvny): + /matrix-js-sdk@31.0.0(patch_hash=xllgzidppgfy66ho7r7op3c7wa): resolution: {integrity: sha512-2TqDwEK34NFS0uiOti02CBCupwJcAIxWarOSD0yIrgMpIwSVNB795jEnXxNXz+bgPKsepDmiqeg2DrlinIoW1w==} engines: {node: '>=18.0.0'} dependencies: From a6e4ab32340ad1bf8eaf63e041f376b912d22221 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Mar 2025 23:06:46 +0700 Subject: [PATCH 10/22] Add test to load more rooms --- .../components/ai-assistant/past-sessions.gts | 5 +- .../host/app/services/matrix-sdk-loader.ts | 6 ++ packages/host/app/services/matrix-service.ts | 14 ++-- .../tests/acceptance/ai-assistant-test.gts | 39 ++++++++++- packages/host/tests/helpers/mock-matrix.ts | 10 ++- .../host/tests/helpers/mock-matrix/_client.ts | 67 ++++++++++++++++++- .../helpers/mock-matrix/_sliding-sync.ts | 65 ++++++++++++++++++ 7 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 packages/host/tests/helpers/mock-matrix/_sliding-sync.ts diff --git a/packages/host/app/components/ai-assistant/past-sessions.gts b/packages/host/app/components/ai-assistant/past-sessions.gts index d9bd3c4169..d18e4eae3e 100644 --- a/packages/host/app/components/ai-assistant/past-sessions.gts +++ b/packages/host/app/components/ai-assistant/past-sessions.gts @@ -69,7 +69,10 @@ export default class AiAssistantPastSessionsList extends Component { {{/each}} {{#if this.matrixService.loadingMoreAIRooms}} -
  • +
  • document.querySelectorAll('[data-test-joined-room]').length === 16, + ); + assert.dom('[data-test-joined-room]').exists({ count: 16 }); + }); }); diff --git a/packages/host/tests/helpers/mock-matrix.ts b/packages/host/tests/helpers/mock-matrix.ts index 12ed97b176..42c5cd0a45 100644 --- a/packages/host/tests/helpers/mock-matrix.ts +++ b/packages/host/tests/helpers/mock-matrix.ts @@ -6,6 +6,7 @@ import type MatrixService from '@cardstack/host/services/matrix-service'; import MessageService from '@cardstack/host/services/message-service'; import { MockSDK } from './mock-matrix/_sdk'; +import { MockSlidingSync } from './mock-matrix/_sliding-sync'; import { MockUtils } from './mock-matrix/_utils'; export interface Config { @@ -23,10 +24,16 @@ export function setupMockMatrix( hooks: NestedHooks, opts: Config = {}, ): MockUtils { - let testState: { owner?: Owner; sdk?: MockSDK; opts?: Config } = { + let testState: { + owner?: Owner; + sdk?: MockSDK; + opts?: Config; + slidingSync?: SlidingSync; + } = { owner: undefined, sdk: undefined, opts: undefined, + slidingSync: undefined, }; let mockUtils = new MockUtils(testState); @@ -77,6 +84,7 @@ export function setupMockMatrix( async load() { return sdk; }, + SlidingSync: MockSlidingSync, }, { instantiate: false, diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index bb2f291cf0..299730e6ef 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -2,6 +2,11 @@ import { MatrixEvent } from 'matrix-js-sdk'; import * as MatrixSDK from 'matrix-js-sdk'; +import { + MSC3575SlidingSyncRequest, + MSC3575SlidingSyncResponse, +} from 'matrix-js-sdk/lib/sliding-sync'; + import { baseRealm, unixTime } from '@cardstack/runtime-common'; import { @@ -57,8 +62,12 @@ export class MockClient implements ExtendedClient { } async startClient( - _opts?: MatrixSDK.IStartClientOpts | undefined, + opts?: MatrixSDK.IStartClientOpts | undefined, ): Promise { + if (opts?.slidingSync) { + await opts.slidingSync.start(); + } + this.serverState.onEvent((serverEvent: IEvent) => { this.emitEvent(new MatrixEvent(serverEvent)); }); @@ -619,4 +628,60 @@ export class MockClient implements ExtendedClient { mxcUrlToHttp(mxcUrl: string): string { return mxcUrl.replace('mxc://', 'http://mock-server/'); } + + async slidingSync( + req: MSC3575SlidingSyncRequest, + _proxyBaseUrl: string, + _signal: AbortSignal, + ): Promise { + let listKey = Object.keys(req.lists || {})[0]; + if (!listKey || !req.lists?.[listKey]?.ranges?.[0]) { + return Promise.resolve({ + pos: '0', + lists: {}, + rooms: {}, + extensions: {}, + }); + } + + let [start, end] = req.lists[listKey].ranges[0]; + let roomsInRange = this.serverState.rooms + .filter((r) => r.id.includes('mock')) + .slice(start, end + 1); + + let response: MSC3575SlidingSyncResponse = { + pos: String(Date.now()), + lists: { + [listKey]: { + count: this.serverState.rooms.length, + ops: [ + { + op: 'SYNC', + range: [start, end], + room_ids: roomsInRange.map((r) => r.id), + }, + ], + }, + }, + rooms: {}, + extensions: {}, + }; + + roomsInRange.forEach((room) => { + response.rooms[room.id] = { + name: + this.serverState.getRoomState(room.id, 'm.room.name', '')?.content + ?.name ?? 'room', + required_state: [], + timeline: this.serverState.getRoomEvents(room.id), + notification_count: 0, + highlight_count: 0, + joined_count: 1, + invited_count: 0, + initial: true, + }; + }); + + return Promise.resolve(response); + } } diff --git a/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts b/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts new file mode 100644 index 0000000000..1b352acbfb --- /dev/null +++ b/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts @@ -0,0 +1,65 @@ +import { MatrixEvent } from 'matrix-js-sdk'; +import * as MatrixSDK from 'matrix-js-sdk'; +import { + SlidingSync, + SlidingSyncEvent, + SlidingSyncState, +} from 'matrix-js-sdk/lib/sliding-sync'; + +export class MockSlidingSync extends SlidingSync { + private lifecycleCallbacks: Function[] = []; + private listCallbacks: Function[] = []; + + on(event: string, callback: Function) { + if (event === SlidingSyncEvent.Lifecycle) { + this.lifecycleCallbacks.push(callback); + } + if (event === SlidingSyncEvent.List) { + this.listCallbacks.push(callback); + } + return this; + } + + emit(event: string, ...args: any[]) { + if (event === SlidingSyncEvent.Lifecycle) { + this.lifecycleCallbacks.forEach((cb) => cb(...args)); + } + if (event === SlidingSyncEvent.List) { + this.listCallbacks.forEach((cb) => cb(...args)); + } + return true; + } + + async start() { + let aiRoomList = this.getListParams('ai-room'); + if (!aiRoomList) { + return; + } + let slidingResponse = await this.client.slidingSync( + { + lists: { ['ai-room']: aiRoomList }, + room_subscriptions: undefined, + }, + '', + {} as any, + ); + + this.emit( + SlidingSyncEvent.Lifecycle, + SlidingSyncState.Complete, + slidingResponse, + ); + + Object.values(slidingResponse.rooms ?? {}).forEach( + (room: MSC3575RoomData) => { + room.timeline.forEach((event: MatrixSDK.IRoomEvent) => { + this.client.emitEvent(new MatrixEvent(event)); + }); + }, + ); + } + + resend() { + this.start(); + } +} From 52ce61fac57e943312f4d0737520d21b39a6af15 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 18 Mar 2025 23:49:26 +0700 Subject: [PATCH 11/22] lint fix --- packages/host/tests/helpers/mock-matrix.ts | 2 -- .../host/tests/helpers/mock-matrix/_client.ts | 7 ++++- .../helpers/mock-matrix/_sliding-sync.ts | 30 +++++++++++-------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/host/tests/helpers/mock-matrix.ts b/packages/host/tests/helpers/mock-matrix.ts index 42c5cd0a45..69d1e9c0b9 100644 --- a/packages/host/tests/helpers/mock-matrix.ts +++ b/packages/host/tests/helpers/mock-matrix.ts @@ -28,12 +28,10 @@ export function setupMockMatrix( owner?: Owner; sdk?: MockSDK; opts?: Config; - slidingSync?: SlidingSync; } = { owner: undefined, sdk: undefined, opts: undefined, - slidingSync: undefined, }; let mockUtils = new MockUtils(testState); diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 299730e6ef..6aa2687f0b 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -668,18 +668,23 @@ export class MockClient implements ExtendedClient { }; roomsInRange.forEach((room) => { + let timeline = this.serverState.getRoomEvents(room.id); response.rooms[room.id] = { name: this.serverState.getRoomState(room.id, 'm.room.name', '')?.content ?.name ?? 'room', required_state: [], - timeline: this.serverState.getRoomEvents(room.id), + timeline, notification_count: 0, highlight_count: 0, joined_count: 1, invited_count: 0, initial: true, }; + + timeline.forEach((event: MatrixSDK.IRoomEvent) => { + this.emitEvent(new MatrixEvent(event)); + }); }); return Promise.resolve(response); diff --git a/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts b/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts index 1b352acbfb..e9a8a08589 100644 --- a/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts +++ b/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts @@ -1,15 +1,28 @@ -import { MatrixEvent } from 'matrix-js-sdk'; import * as MatrixSDK from 'matrix-js-sdk'; import { SlidingSync, SlidingSyncEvent, SlidingSyncState, + MSC3575List, + MSC3575RoomSubscription, } from 'matrix-js-sdk/lib/sliding-sync'; export class MockSlidingSync extends SlidingSync { + private _client: MatrixSDK.MatrixClient; private lifecycleCallbacks: Function[] = []; private listCallbacks: Function[] = []; + constructor( + proxyBaseUrl: string, + lists: Map, + roomSubscriptionInfo: MSC3575RoomSubscription, + client: MatrixSDK.MatrixClient, + timeoutMS: number, + ) { + super(proxyBaseUrl, lists, roomSubscriptionInfo, client, timeoutMS); + this._client = client; + } + on(event: string, callback: Function) { if (event === SlidingSyncEvent.Lifecycle) { this.lifecycleCallbacks.push(callback); @@ -35,7 +48,7 @@ export class MockSlidingSync extends SlidingSync { if (!aiRoomList) { return; } - let slidingResponse = await this.client.slidingSync( + let slidingResponse = await this._client.slidingSync( { lists: { ['ai-room']: aiRoomList }, room_subscriptions: undefined, @@ -49,17 +62,10 @@ export class MockSlidingSync extends SlidingSync { SlidingSyncState.Complete, slidingResponse, ); - - Object.values(slidingResponse.rooms ?? {}).forEach( - (room: MSC3575RoomData) => { - room.timeline.forEach((event: MatrixSDK.IRoomEvent) => { - this.client.emitEvent(new MatrixEvent(event)); - }); - }, - ); } - resend() { - this.start(); + async resend() { + await this.start(); + return ''; } } From 1576b131f6e5b2abbcdc3cbd751bddda0f1c6f01 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Wed, 19 Mar 2025 16:46:22 +0700 Subject: [PATCH 12/22] Fix host tests --- packages/host/app/services/matrix-service.ts | 19 +++++++++++++++++ .../tests/acceptance/ai-assistant-test.gts | 8 +++---- .../host/tests/acceptance/commands-test.gts | 21 ++++++++++++++----- pnpm-lock.yaml | 15 ++++++------- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 4690c057ec..a7b60a933d 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -1234,15 +1234,34 @@ export default class MatrixService extends Service { } } + let startTime = Date.now(); + let checkCount = 0; + let timeline = room.getLiveTimeline(); let events = timeline.getEvents(); await new Promise((resolve) => { let checkEvents = () => { + checkCount++; + let elapsedTime = Date.now() - startTime; let allEventsConsumed = events.every((event) => roomData.events.some((e) => e.event_id === event.getId()), ); + console.log( + `[Matrix Service] Waiting for events... (${elapsedTime}ms, attempt ${checkCount})`, + `\n - Events to consume: ${events.length}`, + `\n - Events consumed: ${ + events.filter((event) => + roomData.events.some((e) => e.event_id === event.getId()), + ).length + }`, + `\n - All consumed: ${allEventsConsumed}`, + ); + if (allEventsConsumed) { + console.log( + `[Matrix Service] All events consumed after ${elapsedTime}ms and ${checkCount} checks`, + ); resolve(); } else { setTimeout(checkEvents, 100); diff --git a/packages/host/tests/acceptance/ai-assistant-test.gts b/packages/host/tests/acceptance/ai-assistant-test.gts index 5f794daeb9..7b76c0dced 100644 --- a/packages/host/tests/acceptance/ai-assistant-test.gts +++ b/packages/host/tests/acceptance/ai-assistant-test.gts @@ -275,7 +275,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { await click('[data-test-llm-select-item="google/gemini-pro-1.5"]'); assert.dom('[data-test-llm-select-selected]').hasText('gemini-pro-1.5'); - let roomState = getRoomState('mock_room_1', APP_BOXEL_ACTIVE_LLM, ''); + let roomState = getRoomState(matrixRoomId, APP_BOXEL_ACTIVE_LLM, ''); assert.strictEqual(roomState.model, 'google/gemini-pro-1.5'); }); @@ -291,9 +291,9 @@ module('Acceptance | AI Assistant tests', function (hooks) { ], }); + await click('[data-test-open-ai-assistant]'); await click('[data-test-submode-switcher] button'); await click('[data-test-boxel-menu-item-text="Code"]'); - await click('[data-test-open-ai-assistant]'); assert.dom('[data-test-llm-select-selected]').hasText('claude-3.5-sonnet'); createAndJoinRoom({ @@ -302,8 +302,8 @@ module('Acceptance | AI Assistant tests', function (hooks) { }); await click('[data-test-past-sessions-button]'); - await waitFor("[data-test-enter-room='mock_room_2']"); - await click('[data-test-enter-room="mock_room_2"]'); + await waitFor("[data-test-enter-room='mock_room_1']"); + await click('[data-test-enter-room="mock_room_1"]'); assert.dom('[data-test-llm-select-selected]').hasText('claude-3.5-sonnet'); }); diff --git a/packages/host/tests/acceptance/commands-test.gts b/packages/host/tests/acceptance/commands-test.gts index 1d2adc8956..7bb487d6b8 100644 --- a/packages/host/tests/acceptance/commands-test.gts +++ b/packages/host/tests/acceptance/commands-test.gts @@ -10,6 +10,8 @@ import { settled, } from '@ember/test-helpers'; +import { fillIn } from '@ember/test-helpers'; + import { module, test } from 'qunit'; import { GridContainer } from '@cardstack/boxel-ui/components'; @@ -317,7 +319,7 @@ module('Acceptance | Commands tests', function (hooks) { ); await sendAiAssistantMessageCommand.execute({ prompt: 'Please delay', - roomId: 'mock_room_1', + roomId: getRoomIds().pop()!, commands: [{ command: sleepCommand, autoExecute: true }], }); await sleepCommand.execute(new ScheduleMeetingInput()); @@ -359,7 +361,7 @@ module('Acceptance | Commands tests', function (hooks) { commandContext, ); await openAiAssistantRoomCommand.execute({ - roomId: 'mock_room_1', + roomId: getRoomIds().pop()!, }); }; @@ -681,8 +683,8 @@ module('Acceptance | Commands tests', function (hooks) { `[data-test-stack-card="${testRealmURL}index"] [data-test-cards-grid-item="${testCard}"]`, ); await click('[data-test-delay-button]'); - await waitUntil(() => getRoomIds().includes('mock_room_1')); - let roomId = 'mock_room_1'; + await waitUntil(() => getRoomIds().length > 0); + let roomId = getRoomIds().pop()!; let message = getRoomEvents(roomId).pop()!; let boxelMessageData = JSON.parse(message.content.data); let toolName = boxelMessageData.context.tools[0].function.name; @@ -1105,11 +1107,20 @@ module('Acceptance | Commands tests', function (hooks) { ], ], }); + let roomId = getRoomIds().pop()!; // open assistant, ShowCard command is part of default CardEditing skill await click('[data-test-open-ai-assistant]'); + // Need to create a new room so this new room will include skills card + await fillIn( + '[data-test-message-field]', + 'Test message to enable new session button', + ); + await click('[data-test-send-message-btn]'); + await click('[data-test-create-room-btn]'); + // simulate message - let roomId = getRoomIds().pop()!; + roomId = getRoomIds().pop()!; simulateRemoteMessage(roomId, '@aibot:localhost', { body: 'Show the card', msgtype: APP_BOXEL_MESSAGE_MSGTYPE, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d686bbdc05..4a6913b671 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,7 +79,7 @@ importers: version: 6.3.0 ember-resources: specifier: ^6.5.1 - version: 6.5.1(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-source@5.4.1) + version: 6.5.1(@ember/test-waiters@3.0.2)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-concurrency@3.1.1)(ember-source@5.4.1) ember-source: specifier: ~5.4.0 version: 5.4.1(patch_hash=oko46qetwhgpor6xgs7vahyney)(@babel/core@7.24.3)(@glimmer/component@1.1.2)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.89.0) @@ -234,7 +234,7 @@ importers: version: 4.1.0(ember-source@5.4.1) ember-resources: specifier: ^6.5.1 - version: 6.5.1(@ember/test-waiters@3.0.2)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-concurrency@3.1.1)(ember-source@5.4.1) + version: 6.5.1(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-concurrency@3.1.1)(ember-source@5.4.1) ember-template-lint: specifier: ^5.11.2 version: 5.11.2 @@ -699,7 +699,7 @@ importers: version: 11.0.1(ember-source@5.4.1) ember-resources: specifier: ^6.5.1 - version: 6.5.1(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-source@5.4.1) + version: 6.5.1(@ember/test-waiters@3.0.2)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-concurrency@3.1.1)(ember-source@5.4.1) ember-source: specifier: ^5.4.0 version: 5.4.1(patch_hash=oko46qetwhgpor6xgs7vahyney)(@babel/core@7.24.3)(@glimmer/component@1.1.2)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.89.0) @@ -1127,7 +1127,7 @@ importers: version: 11.0.1(ember-source@5.4.1) ember-resources: specifier: ^6.5.1 - version: 6.5.1(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-source@5.4.1) + version: 6.5.1(@ember/test-waiters@3.0.2)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-concurrency@3.1.1)(ember-source@5.4.1) ember-source: specifier: ^5.4.0 version: 5.4.1(patch_hash=oko46qetwhgpor6xgs7vahyney)(@babel/core@7.24.3)(@glimmer/component@1.1.2)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.89.0) @@ -14411,7 +14411,7 @@ packages: '@babel/runtime': 7.22.11 '@ember/test-waiters': 3.0.2 '@embroider/addon-shim': 1.8.9 - '@embroider/macros': 1.16.9(@glint/template@1.3.0) + '@embroider/macros': 1.16.5(@glint/template@1.3.0) '@glimmer/component': 1.1.2(@babel/core@7.24.3) '@glimmer/tracking': 1.1.2 '@glint/template': 1.3.0 @@ -14422,7 +14422,7 @@ packages: - supports-color dev: true - /ember-resources@6.5.1(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-source@5.4.1): + /ember-resources@6.5.1(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(@glint/template@1.3.0)(ember-concurrency@3.1.1)(ember-source@5.4.1): resolution: {integrity: sha512-8eEdbSE0sioqjpB2CWw/dKF4Ftfe9tbebuSUfMDBmP3xxTIvxTDbvDnbd8A0IbQ0z/iQt6Va+/5cadzvkbMZtg==} peerDependencies: '@ember/test-waiters': ^3.0.0 @@ -14441,11 +14441,12 @@ packages: dependencies: '@babel/runtime': 7.22.11 '@embroider/addon-shim': 1.8.9 - '@embroider/macros': 1.16.5(@glint/template@1.3.0) + '@embroider/macros': 1.16.9(@glint/template@1.3.0) '@glimmer/component': 1.1.2(@babel/core@7.24.3) '@glimmer/tracking': 1.1.2 '@glint/template': 1.3.0 ember-async-data: 1.0.3(ember-source@5.4.1) + ember-concurrency: 3.1.1(@babel/core@7.24.3)(ember-source@5.4.1) ember-source: 5.4.1(patch_hash=oko46qetwhgpor6xgs7vahyney)(@babel/core@7.24.3)(@glimmer/component@1.1.2)(@glint/template@1.3.0)(rsvp@4.8.5)(webpack@5.89.0) transitivePeerDependencies: - supports-color From 8c5705ed47625b9431a4522a637a5bd6340a2fa2 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 20 Mar 2025 00:16:01 +0700 Subject: [PATCH 13/22] Execute loadAlltimelineEvents once at a time --- packages/host/app/services/matrix-service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 626c0d6a80..9e1f64803f 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -1234,8 +1234,12 @@ export default class MatrixService extends Service { throw new Error(`Cannot find room with id ${roomId}`); } - this.timelineLoadingState.set(roomId, true); + if (this.timelineLoadingState.get(roomId)) { + return; + } + let token = waiter.beginAsync(); + this.timelineLoadingState.set(roomId, true); try { while (room.oldState.paginationToken != null) { await this.client.scrollback(room); From bbbfed9eed99dcdd2bdf1b46349a171046b269e3 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 20 Mar 2025 10:55:43 +0700 Subject: [PATCH 14/22] Update _client and _sliding-sync --- packages/host/app/services/matrix-service.ts | 11 +-- .../tests/acceptance/ai-assistant-test.gts | 1 + .../host/tests/helpers/mock-matrix/_client.ts | 86 ++++++++++--------- .../helpers/mock-matrix/_sliding-sync.ts | 14 ++- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 9e1f64803f..f89282856f 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -2,7 +2,6 @@ import type Owner from '@ember/owner'; import type RouterService from '@ember/routing/router-service'; import { debounce } from '@ember/runloop'; import Service, { service } from '@ember/service'; -import { buildWaiter } from '@ember/test-waiters'; import { cached, tracked } from '@glimmer/tracking'; import { restartableTask, task } from 'ember-concurrency'; @@ -129,7 +128,6 @@ const SLIDING_SYNC_LIST_RANGE_END = 9; const SLIDING_SYNC_LIST_TIMELINE_LIMIT = 1; const realmEventsLogger = logger('realm:events'); -const waiter = buildWaiter('matrix-service:waiter'); export type OperatorModeContext = { submode: Submode; @@ -201,8 +199,7 @@ export default class MatrixService extends Service { set currentRoomId(value: string | undefined) { this._currentRoomId = value; if (value) { - this.loadAllTimelineEvents(value); - window.localStorage.setItem(CurrentRoomIdPersistenceKey, value); + this.loadAllTimelineEvents.perform(value); } else { window.localStorage.removeItem(CurrentRoomIdPersistenceKey); } @@ -1226,7 +1223,7 @@ export default class MatrixService extends Service { return await this.client.isUsernameAvailable(username); } - private async loadAllTimelineEvents(roomId: string) { + private loadAllTimelineEvents = restartableTask(async (roomId: string) => { let roomData = this.ensureRoomData(roomId); let room = this.client.getRoom(roomId); @@ -1238,7 +1235,6 @@ export default class MatrixService extends Service { return; } - let token = waiter.beginAsync(); this.timelineLoadingState.set(roomId, true); try { while (room.oldState.paginationToken != null) { @@ -1287,9 +1283,8 @@ export default class MatrixService extends Service { }); } finally { this.timelineLoadingState.set(roomId, false); - waiter.endAsync(token); } - } + }); get isLoadingTimeline() { if (!this.currentRoomId) { diff --git a/packages/host/tests/acceptance/ai-assistant-test.gts b/packages/host/tests/acceptance/ai-assistant-test.gts index 7b76c0dced..f40ba9fe05 100644 --- a/packages/host/tests/acceptance/ai-assistant-test.gts +++ b/packages/host/tests/acceptance/ai-assistant-test.gts @@ -183,6 +183,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { ], }); await click('[data-test-open-ai-assistant]'); + await waitFor('[data-test-message-field]'); const testCard = `${testRealmURL}Person/hassan`; for (let i = 1; i <= 3; i++) { diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 56c01e1900..64f7ced372 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -634,26 +634,45 @@ export class MockClient implements ExtendedClient { _proxyBaseUrl: string, _signal: AbortSignal, ): Promise { - let listKey = Object.keys(req.lists || {})[0]; - if (!listKey || !req.lists?.[listKey]?.ranges?.[0]) { - return Promise.resolve({ - pos: '0', - lists: {}, - rooms: {}, - extensions: {}, - }); - } + let lists: MSC3575SlidingSyncResponse['lists'] = {}; + let rooms: MSC3575SlidingSyncResponse['rooms'] = {}; + Object.entries(req.lists || {}).forEach(([listKey, list]) => { + list.ranges.forEach((range) => { + let [start, end] = range; + //currently we only filter rooms using is_dm + let dmRooms = + this.getAccountData('m.direct')?.getContent()?.rooms ?? []; + let roomsInRange = this.serverState.rooms + .filter((r) => r.id.includes('mock')) + .filter((r) => + list.filters?.is_dm + ? dmRooms.includes(r.id) + : !dmRooms.includes(r.id), + ) + .slice(start, end + 1); + + roomsInRange.forEach((room) => { + let timeline = this.serverState.getRoomEvents(room.id); + rooms[room.id] = { + name: + this.serverState.getRoomState(room.id, 'm.room.name', '')?.content + ?.name ?? 'room', + required_state: [], + timeline, + notification_count: 0, + highlight_count: 0, + joined_count: 1, + invited_count: 0, + initial: true, + }; - let [start, end] = req.lists[listKey].ranges[0]; - let roomsInRange = this.serverState.rooms - .filter((r) => r.id.includes('mock')) - .slice(start, end + 1); + timeline.forEach((event: MatrixSDK.IRoomEvent) => { + this.emitEvent(new MatrixEvent(event)); + }); + }); - let response: MSC3575SlidingSyncResponse = { - pos: String(Date.now()), - lists: { - [listKey]: { - count: this.serverState.rooms.length, + lists[listKey] = { + count: roomsInRange.length, ops: [ { op: 'SYNC', @@ -661,32 +680,17 @@ export class MockClient implements ExtendedClient { room_ids: roomsInRange.map((r) => r.id), }, ], - }, - }, - rooms: {}, - extensions: {}, - }; - - roomsInRange.forEach((room) => { - let timeline = this.serverState.getRoomEvents(room.id); - response.rooms[room.id] = { - name: - this.serverState.getRoomState(room.id, 'm.room.name', '')?.content - ?.name ?? 'room', - required_state: [], - timeline, - notification_count: 0, - highlight_count: 0, - joined_count: 1, - invited_count: 0, - initial: true, - }; - - timeline.forEach((event: MatrixSDK.IRoomEvent) => { - this.emitEvent(new MatrixEvent(event)); + }; }); }); + let response: MSC3575SlidingSyncResponse = { + pos: String(Date.now()), + lists, + rooms, + extensions: {}, + }; + return Promise.resolve(response); } diff --git a/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts b/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts index e9a8a08589..7af6bec014 100644 --- a/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts +++ b/packages/host/tests/helpers/mock-matrix/_sliding-sync.ts @@ -9,6 +9,7 @@ import { export class MockSlidingSync extends SlidingSync { private _client: MatrixSDK.MatrixClient; + private _lists: Record; private lifecycleCallbacks: Function[] = []; private listCallbacks: Function[] = []; @@ -21,6 +22,7 @@ export class MockSlidingSync extends SlidingSync { ) { super(proxyBaseUrl, lists, roomSubscriptionInfo, client, timeoutMS); this._client = client; + this._lists = Object.fromEntries(lists); } on(event: string, callback: Function) { @@ -44,13 +46,12 @@ export class MockSlidingSync extends SlidingSync { } async start() { - let aiRoomList = this.getListParams('ai-room'); - if (!aiRoomList) { + if (!this._lists) { return; } let slidingResponse = await this._client.slidingSync( { - lists: { ['ai-room']: aiRoomList }, + lists: this._lists, room_subscriptions: undefined, }, '', @@ -64,8 +65,13 @@ export class MockSlidingSync extends SlidingSync { ); } + async setListRanges(listKey: string, ranges: number[][]) { + this._lists[listKey].ranges = ranges; + return await this.resend(); + } + async resend() { await this.start(); - return ''; + return Promise.resolve(''); } } From 9020bf4c452435714b3a7650118f3e90baff9415 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 20 Mar 2025 11:41:21 +0700 Subject: [PATCH 15/22] Revert unnecessary updates --- packages/host/app/components/matrix/room.gts | 12 ++++++--- packages/host/app/services/matrix-service.ts | 26 ++++--------------- .../tests/acceptance/ai-assistant-test.gts | 9 +++++-- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index d028acaf4f..06ba414dbf 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -29,7 +29,7 @@ import { TrackedObject, TrackedSet } from 'tracked-built-ins'; import { v4 as uuidv4 } from 'uuid'; import { BoxelButton, LoadingIndicator } from '@cardstack/boxel-ui/components'; -import { eq, not, or } from '@cardstack/boxel-ui/helpers'; +import { and, eq, not, or } from '@cardstack/boxel-ui/helpers'; import { type getCard, @@ -87,8 +87,14 @@ export default class Room extends Component { {{#if (not this.doMatrixEventFlush.isRunning)}}
    diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index f89282856f..a5256e011c 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -200,6 +200,7 @@ export default class MatrixService extends Service { this._currentRoomId = value; if (value) { this.loadAllTimelineEvents.perform(value); + window.localStorage.setItem(CurrentRoomIdPersistenceKey, value); } else { window.localStorage.removeItem(CurrentRoomIdPersistenceKey); } @@ -1226,8 +1227,9 @@ export default class MatrixService extends Service { private loadAllTimelineEvents = restartableTask(async (roomId: string) => { let roomData = this.ensureRoomData(roomId); let room = this.client.getRoom(roomId); + let roomResource = this.roomResources.get(roomId); - if (!room) { + if (!room || !roomResource) { throw new Error(`Cannot find room with id ${roomId}`); } @@ -1245,34 +1247,15 @@ export default class MatrixService extends Service { } } - let startTime = Date.now(); - let checkCount = 0; - + // Wait for all events to be loaded in roomResource let timeline = room.getLiveTimeline(); let events = timeline.getEvents(); await new Promise((resolve) => { let checkEvents = () => { - checkCount++; - let elapsedTime = Date.now() - startTime; let allEventsConsumed = events.every((event) => roomData.events.some((e) => e.event_id === event.getId()), ); - - console.log( - `[Matrix Service] Waiting for events... (${elapsedTime}ms, attempt ${checkCount})`, - `\n - Events to consume: ${events.length}`, - `\n - Events consumed: ${ - events.filter((event) => - roomData.events.some((e) => e.event_id === event.getId()), - ).length - }`, - `\n - All consumed: ${allEventsConsumed}`, - ); - if (allEventsConsumed) { - console.log( - `[Matrix Service] All events consumed after ${elapsedTime}ms and ${checkCount} checks`, - ); resolve(); } else { setTimeout(checkEvents, 100); @@ -1281,6 +1264,7 @@ export default class MatrixService extends Service { checkEvents(); }); + await this.roomResources.get(roomId)?.loading; } finally { this.timelineLoadingState.set(roomId, false); } diff --git a/packages/host/tests/acceptance/ai-assistant-test.gts b/packages/host/tests/acceptance/ai-assistant-test.gts index f40ba9fe05..41cb4bcf16 100644 --- a/packages/host/tests/acceptance/ai-assistant-test.gts +++ b/packages/host/tests/acceptance/ai-assistant-test.gts @@ -183,7 +183,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { ], }); await click('[data-test-open-ai-assistant]'); - await waitFor('[data-test-message-field]'); + await waitFor(`[data-room-settled]`); const testCard = `${testRealmURL}Person/hassan`; for (let i = 1; i <= 3; i++) { @@ -261,7 +261,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { ], }); await click('[data-test-open-ai-assistant]'); - + await waitFor(`[data-room-settled]`); assert .dom('[data-test-llm-select-selected]') .hasText(DEFAULT_LLM.split('/')[1]); @@ -293,6 +293,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { }); await click('[data-test-open-ai-assistant]'); + await waitFor(`[data-room-settled]`); await click('[data-test-submode-switcher] button'); await click('[data-test-boxel-menu-item-text="Code"]'); assert.dom('[data-test-llm-select-selected]').hasText('claude-3.5-sonnet'); @@ -326,6 +327,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { '[data-test-cards-grid-item="http://test-realm/test/Person/fadhlan"]', ); await click('[data-test-open-ai-assistant]'); + await waitFor(`[data-room-settled]`); assert.dom('[data-test-autoattached-file]').doesNotExist(); assert.dom('[data-test-autoattached-card]').exists(); await click('[data-test-submode-switcher] > [data-test-boxel-button]'); @@ -348,6 +350,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { }); await click('[data-test-open-ai-assistant]'); + await waitFor(`[data-room-settled]`); assert.dom('[data-test-choose-file-btn]').hasText('Attach File'); await click('[data-test-choose-file-btn]'); @@ -412,6 +415,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { ], }); await click('[data-test-open-ai-assistant]'); + await waitFor(`[data-room-settled]`); assert.dom('[data-test-choose-file-btn]').hasText('Attach File'); await click('[data-test-file="person.gts"]'); @@ -453,6 +457,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { }); await click('[data-test-open-ai-assistant]'); + await waitFor(`[data-room-settled]`); await click('[data-test-past-sessions-button]'); assert.dom('[data-test-past-sessions]').exists(); From 5aa449b1993b5fe598a717455307d4283a77e3b9 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 20 Mar 2025 12:47:48 +0700 Subject: [PATCH 16/22] Remove unnecessary disabled --- .../components/ai-assistant/attachment-picker/index.gts | 7 +++---- packages/host/app/components/matrix/room.gts | 8 ++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/host/app/components/ai-assistant/attachment-picker/index.gts b/packages/host/app/components/ai-assistant/attachment-picker/index.gts index 7911833282..eb2d59e3d0 100644 --- a/packages/host/app/components/ai-assistant/attachment-picker/index.gts +++ b/packages/host/app/components/ai-assistant/attachment-picker/index.gts @@ -8,7 +8,7 @@ import { restartableTask } from 'ember-concurrency'; import { TrackedSet } from 'tracked-built-ins'; import { AddButton, Tooltip, Pill } from '@cardstack/boxel-ui/components'; -import { and, cn, eq, gt, not, or } from '@cardstack/boxel-ui/helpers'; +import { and, cn, eq, gt, not } from '@cardstack/boxel-ui/helpers'; import { chooseCard, @@ -39,7 +39,6 @@ interface Signature { submode: Submode; maxNumberOfItemsToAttach?: number; autoAttachedCardTooltipMessage?: string; - disabled?: boolean; }; } @@ -120,7 +119,7 @@ export default class AiAssistantAttachmentPicker extends Component { @iconWidth='14' @iconHeight='14' {{on 'click' this.chooseFile}} - @disabled={{(or this.doChooseFile.isRunning @disabled)}} + @disabled={{this.doChooseFile.isRunning}} data-test-choose-file-btn > @@ -134,7 +133,7 @@ export default class AiAssistantAttachmentPicker extends Component { @iconWidth='14' @iconHeight='14' {{on 'click' this.chooseCard}} - @disabled={{(or this.doChooseCard.isRunning @disabled)}} + @disabled={{this.doChooseCard.isRunning}} data-test-choose-card-btn > diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index 06ba414dbf..c4ef832719 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -29,7 +29,7 @@ import { TrackedObject, TrackedSet } from 'tracked-built-ins'; import { v4 as uuidv4 } from 'uuid'; import { BoxelButton, LoadingIndicator } from '@cardstack/boxel-ui/components'; -import { and, eq, not, or } from '@cardstack/boxel-ui/helpers'; +import { and, eq, not } from '@cardstack/boxel-ui/helpers'; import { type getCard, @@ -172,16 +172,12 @@ export default class Room extends Component { 'Current card is shared automatically' 'Topmost card is shared automatically' }} - @disabled={{this.matrixService.isLoadingTimeline}} /> From b42d1d492cf7c3d58b46745e24fda64ba162dd0f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 20 Mar 2025 13:45:22 +0700 Subject: [PATCH 17/22] Wait for take percySnapshot --- .../tests/integration/components/ai-assistant-panel-test.gts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index 7e3333e61c..26bd7b87c8 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -1241,6 +1241,7 @@ module('Integration | ai-assistant-panel', function (hooks) { await waitFor('[data-test-room]'); assert.dom('[data-test-room-error]').doesNotExist(); assert.dom('[data-test-past-sessions-button]').isEnabled(); + await waitFor('[data-test-room-settled]'); await percySnapshot( 'Integration | ai-assistant-panel | it can handle an error during room creation | new room state', ); From 8bce43f0d38130ae94308e5e908121289c8ed9a8 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 21 Mar 2025 11:43:33 +0700 Subject: [PATCH 18/22] Update based on feedbacks --- .../ai-assistant/past-session-item.gts | 10 +-- .../components/ai-assistant/past-sessions.gts | 2 +- .../host/app/services/matrix-sdk-loader.ts | 2 +- packages/host/app/services/matrix-service.ts | 34 ++++++---- .../tests/acceptance/ai-assistant-test.gts | 5 ++ packages/host/tests/helpers/mock-matrix.ts | 2 +- .../host/tests/helpers/mock-matrix/_client.ts | 45 ++++++++------ .../helpers/mock-matrix/_server-state.ts | 62 ++++++++++--------- .../host/tests/helpers/mock-matrix/_utils.ts | 6 +- packages/realm-server/node-realm.ts | 2 +- packages/realm-server/server.ts | 2 +- .../realm-server/tests/auth-client-test.ts | 2 +- .../matrix-backend-authentication.ts | 4 +- packages/runtime-common/matrix-client.ts | 4 +- packages/runtime-common/realm-auth-client.ts | 6 +- 15 files changed, 103 insertions(+), 85 deletions(-) diff --git a/packages/host/app/components/ai-assistant/past-session-item.gts b/packages/host/app/components/ai-assistant/past-session-item.gts index 0047679c03..65db0d6b89 100644 --- a/packages/host/app/components/ai-assistant/past-session-item.gts +++ b/packages/host/app/components/ai-assistant/past-session-item.gts @@ -206,15 +206,7 @@ export default class PastSessionItem extends Component { } get isStreaming() { - if (!this.args.session.lastMessage) { - return false; - } - - return ( - this.args.session.lastMessage.author.userId !== - this.matrixService.userId && - !this.args.session.lastMessage.isStreamingFinished - ); + return this.args.session.lastMessage?.isStreamingFinished === false; } get hasUnseenMessage() { diff --git a/packages/host/app/components/ai-assistant/past-sessions.gts b/packages/host/app/components/ai-assistant/past-sessions.gts index d18e4eae3e..f4317bfe3f 100644 --- a/packages/host/app/components/ai-assistant/past-sessions.gts +++ b/packages/host/app/components/ai-assistant/past-sessions.gts @@ -68,7 +68,7 @@ export default class AiAssistantPastSessionsList extends Component { {{#each @sessions key='roomId' as |session|}} {{/each}} - {{#if this.matrixService.loadingMoreAIRooms}} + {{#if this.matrixService.isLoadingMoreAIRooms}}
  • = new Set(); - @tracked private isLoadingMoreAIRooms = false; + @tracked private _isLoadingMoreAIRooms = false; constructor(owner: Owner) { super(owner); @@ -555,7 +555,7 @@ export default class MatrixService extends Service { filters: { is_dm: true, }, - timeline_limit: 1, + timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT, required_state: [['*', '*']], }); this.slidingSync = new this.matrixSdkLoader.SlidingSync( @@ -565,7 +565,7 @@ export default class MatrixService extends Service { timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT, }, this.client as any, - environment === 'test' ? 200 : 2000, + 500, ); this.slidingSync.on( SlidingSyncEvent.Lifecycle, @@ -1224,7 +1224,7 @@ export default class MatrixService extends Service { return await this.client.isUsernameAvailable(username); } - private loadAllTimelineEvents = restartableTask(async (roomId: string) => { + private loadAllTimelineEvents = dropTask(async (roomId: string) => { let roomData = this.ensureRoomData(roomId); let room = this.client.getRoom(roomId); let roomResource = this.roomResources.get(roomId); @@ -1669,8 +1669,12 @@ export default class MatrixService extends Service { this.loadMoreAIRoomsTask.perform(); } - private loadMoreAIRoomsTask = restartableTask(async () => { - if (!this.slidingSync) return; + private loadMoreAIRoomsTask = dropTask(async () => { + if (!this.slidingSync) { + throw new Error( + 'To load more AI rooms, sliding sync must be initialized', + ); + } let currentList = this.slidingSync.getListParams( SLIDING_SYNC_AI_ROOM_LIST_NAME, @@ -1686,22 +1690,26 @@ export default class MatrixService extends Service { let newEndRange = currentRange[1] + 10; - this.isLoadingMoreAIRooms = true; + this._isLoadingMoreAIRooms = true; try { await this.slidingSync.setListRanges(SLIDING_SYNC_AI_ROOM_LIST_NAME, [ [0, newEndRange], ]); } finally { - this.isLoadingMoreAIRooms = false; + this._isLoadingMoreAIRooms = false; } }); - get loadingMoreAIRooms() { - return this.isLoadingMoreAIRooms; + get isLoadingMoreAIRooms() { + return this._isLoadingMoreAIRooms; } async loadMoreAuthRooms(realms: string[]) { - if (!this.slidingSync) return; + if (!this.slidingSync) { + throw new Error( + 'To load more auth rooms, sliding sync must be initialized', + ); + } let currentList = this.slidingSync.getListParams( SLIDING_SYNC_AUTH_ROOM_LIST_NAME, diff --git a/packages/host/tests/acceptance/ai-assistant-test.gts b/packages/host/tests/acceptance/ai-assistant-test.gts index 41cb4bcf16..e0fba4e3ee 100644 --- a/packages/host/tests/acceptance/ai-assistant-test.gts +++ b/packages/host/tests/acceptance/ai-assistant-test.gts @@ -35,6 +35,7 @@ import { } from '../helpers/base-realm'; import { setupMockMatrix } from '../helpers/mock-matrix'; +import { getRoomIdForRealmAndUser } from '../helpers/mock-matrix/_utils'; import { setupApplicationTest } from '../helpers/setup'; async function selectCardFromCatalog(cardId: string) { @@ -52,6 +53,10 @@ module('Acceptance | AI Assistant tests', function (hooks) { let mockMatrixUtils = setupMockMatrix(hooks, { loggedInAs: '@testuser:localhost', activeRealms: [baseRealm.url, testRealmURL], + directRooms: [ + getRoomIdForRealmAndUser(testRealmURL, '@testuser:localhost'), + getRoomIdForRealmAndUser(baseRealm.url, '@testuser:localhost'), + ], }); let { createAndJoinRoom, getRoomState } = mockMatrixUtils; diff --git a/packages/host/tests/helpers/mock-matrix.ts b/packages/host/tests/helpers/mock-matrix.ts index 69d1e9c0b9..4633e417b7 100644 --- a/packages/host/tests/helpers/mock-matrix.ts +++ b/packages/host/tests/helpers/mock-matrix.ts @@ -17,7 +17,7 @@ export interface Config { expiresInSec?: number; autostart?: boolean; now?: () => number; - directRooms?: Record; + directRooms?: string[]; } export function setupMockMatrix( diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 64f7ced372..21011cfe55 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -52,9 +52,16 @@ export class MockClient implements ExtendedClient { async getAccountDataFromServer( _eventType: string, ): Promise { - return { - realms: this.sdkOpts.activeRealms ?? [], - } as unknown as T; + if (_eventType === 'm.direct') { + return { + [this.loggedInAs!]: this.sdkOpts.directRooms ?? [], + } as unknown as T; + } else if (_eventType === APP_BOXEL_REALMS_EVENT_TYPE) { + return { + realms: this.sdkOpts.activeRealms ?? [], + } as unknown as T; + } + return null; } get loggedInAs() { @@ -105,6 +112,7 @@ export class MockClient implements ExtendedClient { nonce: nonce++, permissions, }; + let stringifiedHeader = JSON.stringify(header); let stringifiedPayload = JSON.stringify(payload); let headerAndPayload = `${btoa(stringifiedHeader)}.${btoa( @@ -131,7 +139,7 @@ export class MockClient implements ExtendedClient { if (type === APP_BOXEL_REALMS_EVENT_TYPE) { this.sdkOpts.activeRealms = (data as any).realms; } else if (type === 'm.direct') { - this.sdkOpts.directRooms = data as any; + this.sdkOpts.directRooms = (data as any)[this.loggedInAs!]; } else { throw new Error( 'Support for updating this event type in account data is not yet implemented in this mock.', @@ -636,22 +644,21 @@ export class MockClient implements ExtendedClient { ): Promise { let lists: MSC3575SlidingSyncResponse['lists'] = {}; let rooms: MSC3575SlidingSyncResponse['rooms'] = {}; - Object.entries(req.lists || {}).forEach(([listKey, list]) => { - list.ranges.forEach((range) => { - let [start, end] = range; + for (const [listKey, list] of Object.entries(req.lists || {})) { + for (let i = 0; i < list.ranges.length; i++) { + let [start, end] = list.ranges[i]; //currently we only filter rooms using is_dm - let dmRooms = - this.getAccountData('m.direct')?.getContent()?.rooms ?? []; + let dmRooms = (await this.getAccountDataFromServer('m.direct')) ?? {}; let roomsInRange = this.serverState.rooms - .filter((r) => r.id.includes('mock')) .filter((r) => list.filters?.is_dm - ? dmRooms.includes(r.id) - : !dmRooms.includes(r.id), + ? dmRooms[this.loggedInAs!]?.includes(r.id) + : !dmRooms[this.loggedInAs!]?.includes(r.id), ) .slice(start, end + 1); - roomsInRange.forEach((room) => { + for (let j = 0; j < roomsInRange.length; j++) { + let room = roomsInRange[j]; let timeline = this.serverState.getRoomEvents(room.id); rooms[room.id] = { name: @@ -665,11 +672,11 @@ export class MockClient implements ExtendedClient { invited_count: 0, initial: true, }; - - timeline.forEach((event: MatrixSDK.IRoomEvent) => { + for (let k = 0; k < timeline.length; k++) { + let event = timeline[k]; this.emitEvent(new MatrixEvent(event)); - }); - }); + } + } lists[listKey] = { count: roomsInRange.length, @@ -681,8 +688,8 @@ export class MockClient implements ExtendedClient { }, ], }; - }); - }); + } + } let response: MSC3575SlidingSyncResponse = { pos: String(Date.now()), diff --git a/packages/host/tests/helpers/mock-matrix/_server-state.ts b/packages/host/tests/helpers/mock-matrix/_server-state.ts index cc0024e53b..dd398ce226 100644 --- a/packages/host/tests/helpers/mock-matrix/_server-state.ts +++ b/packages/host/tests/helpers/mock-matrix/_server-state.ts @@ -98,39 +98,41 @@ export class ServerState { { origin_server_ts: timestamp, state_key: sender }, ); - this.addRoomEvent( - sender, - { - room_id: roomId, - type: 'm.room.member', - content: { - displayname: 'aibot', - membership: 'invite', + if (!roomId.includes('test-session-room-realm')) { + this.addRoomEvent( + sender, + { + room_id: roomId, + type: 'm.room.member', + content: { + displayname: 'aibot', + membership: 'invite', + }, }, - }, - { - origin_server_ts: timestamp, - // host application expects this for the bot to join the room - state_key: '@aibot:localhost', - }, - ); + { + origin_server_ts: timestamp, + // host application expects this for the bot to join the room + state_key: '@aibot:localhost', + }, + ); - this.addRoomEvent( - '@aibot:localhost', - { - room_id: roomId, - type: 'm.room.member', - content: { - displayname: 'aibot', - membership: 'join', + this.addRoomEvent( + '@aibot:localhost', + { + room_id: roomId, + type: 'm.room.member', + content: { + displayname: 'aibot', + membership: 'join', + }, }, - }, - { - origin_server_ts: timestamp, - // host application expects this for the bot to join the room - state_key: '@aibot:localhost', - }, - ); + { + origin_server_ts: timestamp, + // host application expects this for the bot to join the room + state_key: '@aibot:localhost', + }, + ); + } return roomId; } diff --git a/packages/host/tests/helpers/mock-matrix/_utils.ts b/packages/host/tests/helpers/mock-matrix/_utils.ts index 39a8b7817f..f739109eb8 100644 --- a/packages/host/tests/helpers/mock-matrix/_utils.ts +++ b/packages/host/tests/helpers/mock-matrix/_utils.ts @@ -25,7 +25,7 @@ export class MockUtils { }; getRoomIdForRealmAndUser = (realmURL: string, userId: string) => { - return `test-session-room-realm-${realmURL}-user-${userId}`; + return getRoomIdForRealmAndUser(realmURL, userId); }; getRoomState = (roomId: string, eventType: string, stateKey?: string) => { @@ -109,3 +109,7 @@ export class MockUtils { function isRealmEvent(e: IEvent): e is RealmEvent { return e.type === APP_BOXEL_REALM_EVENT_TYPE; } + +export function getRoomIdForRealmAndUser(realmURL: string, userId: string) { + return `test-session-room-realm-${realmURL}-user-${userId}`; +} diff --git a/packages/realm-server/node-realm.ts b/packages/realm-server/node-realm.ts index 3b821dbd22..5c4955adcb 100644 --- a/packages/realm-server/node-realm.ts +++ b/packages/realm-server/node-realm.ts @@ -224,7 +224,7 @@ export class NodeAdapter implements RealmAdapter { try { dmRooms = - (await matrixClient.getAccountData>( + (await matrixClient.getAccountDataFromServer>( 'boxel.session-rooms', )) ?? {}; } catch (e) { diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index cd74ddd78b..1c14b6e4e8 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -461,7 +461,7 @@ export class RealmServer { private sendEvent = async (user: string, eventType: string) => { let dmRooms = - (await this.matrixClient.getAccountData>( + (await this.matrixClient.getAccountDataFromServer>( 'boxel.session-rooms', )) ?? {}; let roomId = dmRooms[user]; diff --git a/packages/realm-server/tests/auth-client-test.ts b/packages/realm-server/tests/auth-client-test.ts index ff2c6e410c..eb4cbd3b27 100644 --- a/packages/realm-server/tests/auth-client-test.ts +++ b/packages/realm-server/tests/auth-client-test.ts @@ -37,7 +37,7 @@ module(basename(__filename), function () { async hashMessageWithSecret(_message: string): Promise { throw new Error('Method not implemented.'); }, - async getAccountData() { + async getAccountDataFromServer() { return {}; }, async setAccountData() { diff --git a/packages/runtime-common/matrix-backend-authentication.ts b/packages/runtime-common/matrix-backend-authentication.ts index b9359c193e..46500d1d3b 100644 --- a/packages/runtime-common/matrix-backend-authentication.ts +++ b/packages/runtime-common/matrix-backend-authentication.ts @@ -71,7 +71,7 @@ export class MatrixBackendAuthentication { } let dmRooms = - (await this.matrixClient.getAccountData>( + (await this.matrixClient.getAccountDataFromServer>( 'boxel.session-rooms', )) ?? {}; let roomId = dmRooms[user]; @@ -136,7 +136,7 @@ export class MatrixBackendAuthentication { } let dmRooms = - (await this.matrixClient.getAccountData>( + (await this.matrixClient.getAccountDataFromServer>( 'boxel.session-rooms', )) ?? {}; let roomId = dmRooms[user]; diff --git a/packages/runtime-common/matrix-client.ts b/packages/runtime-common/matrix-client.ts index 401029eb8a..db4bd19319 100644 --- a/packages/runtime-common/matrix-client.ts +++ b/packages/runtime-common/matrix-client.ts @@ -212,7 +212,7 @@ export class MatrixClient { } } - async getAccountData(type: string) { + async getAccountDataFromServer(type: string) { if (!this.access) { await this.login(); } @@ -222,7 +222,7 @@ export class MatrixClient { )}/account_data/${type}`, ); if (response.status === 404) { - return; + return null; } let json = await response.json(); if (!response.ok) { diff --git a/packages/runtime-common/realm-auth-client.ts b/packages/runtime-common/realm-auth-client.ts index 8e232703a9..8130d893b6 100644 --- a/packages/runtime-common/realm-auth-client.ts +++ b/packages/runtime-common/realm-auth-client.ts @@ -14,7 +14,7 @@ export interface RealmAuthMatrixClientInterface { joinRoom(room: string): Promise; sendEvent(room: string, type: string, content: any): Promise; hashMessageWithSecret(message: string): Promise; - getAccountData(type: string): any; + getAccountDataFromServer(type: string): Promise<{ [k: string]: any } | null>; setAccountData(type: string, data: any): Promise; } @@ -102,8 +102,8 @@ export class RealmAuthClient { if (!rooms.includes(room)) { await this.matrixClient.joinRoom(room); } - - let directRooms = this.matrixClient.getAccountData('m.direct'); + let directRooms = + await this.matrixClient.getAccountDataFromServer('m.direct'); let userId = this.matrixClient.getUserId() as string; if (!directRooms?.[userId]?.includes(room)) { await this.matrixClient.setAccountData('m.direct', { From 682d7ffe5ad2028a965767d27c25bf02aa8515d1 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 21 Mar 2025 12:45:43 +0700 Subject: [PATCH 19/22] Revert unnecassry changes --- .../Author/alice-enwunder.json | 4 +-- .../85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json | 32 ++++++++++++------- .../host/tests/helpers/mock-matrix/_client.ts | 18 ++--------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/experiments-realm/Author/alice-enwunder.json b/packages/experiments-realm/Author/alice-enwunder.json index 371dfc4458..ff03e4b33f 100644 --- a/packages/experiments-realm/Author/alice-enwunder.json +++ b/packages/experiments-realm/Author/alice-enwunder.json @@ -4,9 +4,9 @@ "attributes": { "firstName": "Alice", "lastName": "Enwunder", - "bio": null, + "bio": "Alice Enwunder is a curious explorer of digital wonderlands. After falling down a rabbit hole of code during a particularly sleepy afternoon, she discovered a world where logic puzzles meet creative storytelling. Known for her adventures through technology landscapes, Alice continues to chase digital white rabbits and unbirthday celebrations.\n\nDuring her journey through the digital realm, Alice encountered talking computers that spoke in riddles and doorways that only opened with the right algorithms. She documented these experiences in her journal, which later became the foundation for her groundbreaking work on human-computer interaction. Her favorite adventure involved a chess game with the Red Queen's quantum computer, where pieces existed in multiple states simultaneously.\n\nNow Alice divides her time between teaching at the School of Impossible Things and writing articles that challenge readers to believe six impossible things before breakfast. When not exploring new technological frontiers, she hosts tea parties where attendees discuss paradoxes and puzzles while enjoying digital scones and virtual tea.", "fullBio": null, - "quote": "Curiouser and curiouser", + "quote": "In the garden of technology, the most beautiful flowers grow from the most impossible ideas.", "contactLinks": [ { "label": "X", diff --git a/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json b/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json index bf334b6daa..d5b2df53fc 100644 --- a/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json +++ b/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json @@ -2,25 +2,25 @@ "data": { "type": "card", "attributes": { - "translation": "", - "headline": "", - "slug": null, - "body": "To quote wikipedia:\n\nMicroblogging is a form of blogging using short posts without titles known as microposts[1][2][3] (or status updates on a minority of websites like Meta Platforms'). Microblogs \"allow users to exchange small elements of content such as short sentences, individual images, or video links\",[1] which may be the major reason for their popularity.[4] Some popular social networks such as Twitter, Threads, Mastodon, Tumblr, Koo, and Instagram can be viewed as collections of microblogs.", + "translation": "# Breaking Bad : Un Chef-d'œuvre Télévisuel\n\nCréée par Vince Gilligan et diffusée sur AMC en 2008, Breaking Bad s'impose comme l'une des plus grandes réussites télévisuelles. La série suit Walter White (Bryan Cranston), un professeur de chimie au lycée qui, après avoir été diagnostiqué d'un cancer du poumon en phase terminale, s'associe à Jesse Pinkman (Aaron Paul), son ancien élève, pour produire et vendre de la méthamphétamine afin d'assurer l'avenir financier de sa famille.\n\n## Un Casting Parfait\n\nLe succès de la série doit beaucoup à son casting exceptionnel. Bryan Cranston a livré une performance définissant sa carrière, se transformant d'un homme de famille passif et sous-performant en impitoyable baron de la drogue connu sous le nom de \"Heisenberg\". Son interprétation lui a valu quatre Emmy Awards du meilleur acteur principal. Le personnage de Jesse Pinkman, interprété par Aaron Paul, a évolué d'un grossier dealer de drogue à la boussole morale de la série, ce qui lui a valu trois Emmy du meilleur second rôle.\n\nLe casting secondaire était tout aussi impressionnant, avec Anna Gunn dans le rôle de Skyler, la femme de Walter, Dean Norris dans celui de Hank Schrader, son beau-frère agent de la DEA, Bob Odenkirk dans celui de l'avocat véreux Saul Goodman, Giancarlo Esposito dans celui du calculateur Gus Fring, et Jonathan Banks dans celui du stoïque Mike Ehrmantraut.\n\n## Une Narration Parfaitement Construite\n\nL'arc narratif des cinq saisons de Breaking Bad est un chef-d'œuvre de narration. La série retrace méticuleusement la dégradation morale de Walter, décrite par Gilligan comme \"la transformation de M. Chips en Scarface\". Chaque saison augmente les enjeux :\n\n- **Saison 1** : Walter entre dans le monde de la drogue et affronte ses premiers adversaires\n- **Saison 2** : Le partenariat avec Jesse s'approfondit alors que les complications surgissent\n- **Saison 3** : L'introduction de Gus Fring crée de nouveaux dangers\n- **Saison 4** : Walt et Gus s'engagent dans une bataille d'esprits semblable aux échecs\n- **Saison 5** : Tous les secrets sont révélés dans une conclusion dévastatrice\n\n## Acclamation Critique\n\nBreaking Bad a reçu des éloges universels, remportant 16 Emmy Awards et étant nommée l'une des plus grandes séries télévisées de tous les temps par les critiques. La dernière saison a obtenu un rare 99/100 sur Metacritic, avec un final regardé par plus de 10 millions de téléspectateurs.\n\nL'impact de la série s'est étendu au-delà de sa diffusion, engendrant la série préquelle Better Call Saul et le film suite El Camino, tous deux poursuivant la haute qualité narrative établie dans la série originale.\n\n## Héritage Culturel\n\nBreaking Bad a révolutionné la télévision par sa qualité cinématographique, son développement complexe des personnages et son examen sans concession du rêve américain qui tourne mal. Son influence se retrouve dans d'innombrables séries qui ont suivi, cimentant sa place non seulement comme un divertissement incroyable, mais comme une profonde déclaration artistique sur la moralité, la famille et les conséquences de nos choix.", + "headline": "Breaking Bad: The Complete Series Analysis", + "slug": "breaking-bad-a-television-masterpiece", + "body": "# Breaking Bad: A Masterpiece of Television\n\nCreated by Vince Gilligan and premiering on AMC in 2008, Breaking Bad stands as one of television's greatest achievements. The series follows Walter White (Bryan Cranston), a high school chemistry teacher who, after being diagnosed with terminal lung cancer, partners with former student Jesse Pinkman (Aaron Paul) to produce and sell methamphetamine to secure his family's financial future.\n\n## The Perfect Cast\n\nThe show's success owes much to its exceptional cast. Bryan Cranston delivered a career-defining performance, transforming from an underachieving, passive family man into the ruthless drug kingpin known as \"Heisenberg.\" His portrayal earned him four Emmy Awards for Outstanding Lead Actor. Aaron Paul's Jesse Pinkman evolved from a crude drug dealer to the show's moral compass, earning him three Supporting Actor Emmys.\n\nThe supporting cast was equally impressive, with Anna Gunn as Walter's wife Skyler, Dean Norris as his DEA agent brother-in-law Hank Schrader, Bob Odenkirk as criminal lawyer Saul Goodman, Giancarlo Esposito as the calculating Gus Fring, and Jonathan Banks as the stoic enforcer Mike Ehrmantraut.\n\n## A Perfectly Constructed Narrative\n\nBreaking Bad's five-season arc is a masterclass in storytelling. The series meticulously charts Walter's moral degradation, famously described by Gilligan as \"turning Mr. Chips into Scarface.\" Each season raises the stakes:\n\n- **Season 1**: Walter enters the drug world and confronts his first adversaries\n- **Season 2**: The partnership with Jesse deepens as complications arise\n- **Season 3**: The introduction of Gus Fring creates new dangers\n- **Season 4**: Walt and Gus engage in a chess-like battle of wits\n- **Season A**: All secrets are revealed in a devastating conclusion\n\n## Critical Acclaim\n\nBreaking Bad received universal acclaim, winning 16 Emmy Awards and being named one of the greatest television series of all time by critics. The show's final season achieved a rare 99/100 on Metacritic, with the finale watched by over 10 million viewers.\n\nThe series' impact extended beyond its run, spawning the prequel series Better Call Saul and the sequel film El Camino, both continuing the high quality of storytelling established in the original series.\n\n## Cultural Legacy\n\nBreaking Bad revolutionized television with its cinematic quality, complex character development, and unflinching examination of the American dream gone wrong. Its influence can be seen in countless shows that followed, cementing its place not just as incredible entertainment, but as a profound artistic statement on morality, family, and the consequences of our choices.", "publishDate": null, "featuredImage": { - "imageUrl": null, - "credit": null, - "caption": null, - "altText": null, + "imageUrl": "https://m.media-amazon.com/images/M/MV5BYmQ4YWMxYjUtNjZmYi00MDQ1LWFjMjMtNjA5ZDdiYjdiODU5XkEyXkFqcGdeQXVyMTMzNDExODE5._V1_.jpg", + "credit": "AMC", + "caption": "Official Breaking Bad series poster", + "altText": "Breaking Bad poster featuring Walter White (Bryan Cranston) and Jesse Pinkman (Aaron Paul) with the show's logo", "size": "actual", - "height": null, - "width": null + "height": 1500, + "width": 1000 }, "description": null, "thumbnailURL": null }, "relationships": { - "authorBio": { + "authors": { "links": { "self": null } @@ -29,6 +29,16 @@ "links": { "self": null } + }, + "categories": { + "links": { + "self": null + } + }, + "editors": { + "links": { + "self": null + } } }, "meta": { diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 21011cfe55..a951143ba9 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -148,22 +148,8 @@ export class MockClient implements ExtendedClient { return Promise.resolve({}); } - getAccountData(eventType: string): MatrixEvent | undefined { - if (eventType === 'm.direct') { - return new MatrixEvent({ - type: 'm.direct', - content: this.sdkOpts.directRooms || {}, - }); - } - if (eventType === APP_BOXEL_REALMS_EVENT_TYPE) { - return new MatrixEvent({ - type: APP_BOXEL_REALMS_EVENT_TYPE, - content: { - realms: this.sdkOpts.activeRealms || [], - }, - }); - } - return undefined; + getAccountData(_type: string): Promise { + throw new Error('Method not implemented.'); } addThreePidOnly(_data: MatrixSDK.IAddThreePidOnlyBody): Promise<{}> { From 6b6f0ea434b5a31dd813e9c2b4eb1f49f4a73ba6 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 21 Mar 2025 12:49:47 +0700 Subject: [PATCH 20/22] Use roomResource.processing --- packages/host/app/services/matrix-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index de0d372c69..07a8f0c5fe 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -1264,7 +1264,7 @@ export default class MatrixService extends Service { checkEvents(); }); - await this.roomResources.get(roomId)?.loading; + await this.roomResources.get(roomId)?.processing; } finally { this.timelineLoadingState.set(roomId, false); } From d01bf2b257d5f65bd8e9d784087689b76aeae8f6 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Sun, 23 Mar 2025 14:45:13 +0700 Subject: [PATCH 21/22] Simplified wait for event to be loaded --- packages/host/app/services/matrix-service.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 07a8f0c5fe..e53497a68a 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -1250,20 +1250,8 @@ export default class MatrixService extends Service { // Wait for all events to be loaded in roomResource let timeline = room.getLiveTimeline(); let events = timeline.getEvents(); - await new Promise((resolve) => { - let checkEvents = () => { - let allEventsConsumed = events.every((event) => - roomData.events.some((e) => e.event_id === event.getId()), - ); - if (allEventsConsumed) { - resolve(); - } else { - setTimeout(checkEvents, 100); - } - }; - - checkEvents(); - }); + this.timelineQueue.push(...events.map((e) => ({ event: e }))); + await this.drainTimeline(); await this.roomResources.get(roomId)?.processing; } finally { this.timelineLoadingState.set(roomId, false); From a4fe8c9f77f3dcdc99f37ad25bc40a2de833360c Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Sun, 23 Mar 2025 14:53:24 +0700 Subject: [PATCH 22/22] revert instance updates --- .../Author/alice-enwunder.json | 4 +-- .../85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json | 32 +++++++------------ 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/experiments-realm/Author/alice-enwunder.json b/packages/experiments-realm/Author/alice-enwunder.json index ff03e4b33f..371dfc4458 100644 --- a/packages/experiments-realm/Author/alice-enwunder.json +++ b/packages/experiments-realm/Author/alice-enwunder.json @@ -4,9 +4,9 @@ "attributes": { "firstName": "Alice", "lastName": "Enwunder", - "bio": "Alice Enwunder is a curious explorer of digital wonderlands. After falling down a rabbit hole of code during a particularly sleepy afternoon, she discovered a world where logic puzzles meet creative storytelling. Known for her adventures through technology landscapes, Alice continues to chase digital white rabbits and unbirthday celebrations.\n\nDuring her journey through the digital realm, Alice encountered talking computers that spoke in riddles and doorways that only opened with the right algorithms. She documented these experiences in her journal, which later became the foundation for her groundbreaking work on human-computer interaction. Her favorite adventure involved a chess game with the Red Queen's quantum computer, where pieces existed in multiple states simultaneously.\n\nNow Alice divides her time between teaching at the School of Impossible Things and writing articles that challenge readers to believe six impossible things before breakfast. When not exploring new technological frontiers, she hosts tea parties where attendees discuss paradoxes and puzzles while enjoying digital scones and virtual tea.", + "bio": null, "fullBio": null, - "quote": "In the garden of technology, the most beautiful flowers grow from the most impossible ideas.", + "quote": "Curiouser and curiouser", "contactLinks": [ { "label": "X", diff --git a/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json b/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json index d5b2df53fc..bf334b6daa 100644 --- a/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json +++ b/packages/experiments-realm/BilingualBlog/85de4d7a-e9d8-459d-80a7-d9f0508c9ea1.json @@ -2,25 +2,25 @@ "data": { "type": "card", "attributes": { - "translation": "# Breaking Bad : Un Chef-d'œuvre Télévisuel\n\nCréée par Vince Gilligan et diffusée sur AMC en 2008, Breaking Bad s'impose comme l'une des plus grandes réussites télévisuelles. La série suit Walter White (Bryan Cranston), un professeur de chimie au lycée qui, après avoir été diagnostiqué d'un cancer du poumon en phase terminale, s'associe à Jesse Pinkman (Aaron Paul), son ancien élève, pour produire et vendre de la méthamphétamine afin d'assurer l'avenir financier de sa famille.\n\n## Un Casting Parfait\n\nLe succès de la série doit beaucoup à son casting exceptionnel. Bryan Cranston a livré une performance définissant sa carrière, se transformant d'un homme de famille passif et sous-performant en impitoyable baron de la drogue connu sous le nom de \"Heisenberg\". Son interprétation lui a valu quatre Emmy Awards du meilleur acteur principal. Le personnage de Jesse Pinkman, interprété par Aaron Paul, a évolué d'un grossier dealer de drogue à la boussole morale de la série, ce qui lui a valu trois Emmy du meilleur second rôle.\n\nLe casting secondaire était tout aussi impressionnant, avec Anna Gunn dans le rôle de Skyler, la femme de Walter, Dean Norris dans celui de Hank Schrader, son beau-frère agent de la DEA, Bob Odenkirk dans celui de l'avocat véreux Saul Goodman, Giancarlo Esposito dans celui du calculateur Gus Fring, et Jonathan Banks dans celui du stoïque Mike Ehrmantraut.\n\n## Une Narration Parfaitement Construite\n\nL'arc narratif des cinq saisons de Breaking Bad est un chef-d'œuvre de narration. La série retrace méticuleusement la dégradation morale de Walter, décrite par Gilligan comme \"la transformation de M. Chips en Scarface\". Chaque saison augmente les enjeux :\n\n- **Saison 1** : Walter entre dans le monde de la drogue et affronte ses premiers adversaires\n- **Saison 2** : Le partenariat avec Jesse s'approfondit alors que les complications surgissent\n- **Saison 3** : L'introduction de Gus Fring crée de nouveaux dangers\n- **Saison 4** : Walt et Gus s'engagent dans une bataille d'esprits semblable aux échecs\n- **Saison 5** : Tous les secrets sont révélés dans une conclusion dévastatrice\n\n## Acclamation Critique\n\nBreaking Bad a reçu des éloges universels, remportant 16 Emmy Awards et étant nommée l'une des plus grandes séries télévisées de tous les temps par les critiques. La dernière saison a obtenu un rare 99/100 sur Metacritic, avec un final regardé par plus de 10 millions de téléspectateurs.\n\nL'impact de la série s'est étendu au-delà de sa diffusion, engendrant la série préquelle Better Call Saul et le film suite El Camino, tous deux poursuivant la haute qualité narrative établie dans la série originale.\n\n## Héritage Culturel\n\nBreaking Bad a révolutionné la télévision par sa qualité cinématographique, son développement complexe des personnages et son examen sans concession du rêve américain qui tourne mal. Son influence se retrouve dans d'innombrables séries qui ont suivi, cimentant sa place non seulement comme un divertissement incroyable, mais comme une profonde déclaration artistique sur la moralité, la famille et les conséquences de nos choix.", - "headline": "Breaking Bad: The Complete Series Analysis", - "slug": "breaking-bad-a-television-masterpiece", - "body": "# Breaking Bad: A Masterpiece of Television\n\nCreated by Vince Gilligan and premiering on AMC in 2008, Breaking Bad stands as one of television's greatest achievements. The series follows Walter White (Bryan Cranston), a high school chemistry teacher who, after being diagnosed with terminal lung cancer, partners with former student Jesse Pinkman (Aaron Paul) to produce and sell methamphetamine to secure his family's financial future.\n\n## The Perfect Cast\n\nThe show's success owes much to its exceptional cast. Bryan Cranston delivered a career-defining performance, transforming from an underachieving, passive family man into the ruthless drug kingpin known as \"Heisenberg.\" His portrayal earned him four Emmy Awards for Outstanding Lead Actor. Aaron Paul's Jesse Pinkman evolved from a crude drug dealer to the show's moral compass, earning him three Supporting Actor Emmys.\n\nThe supporting cast was equally impressive, with Anna Gunn as Walter's wife Skyler, Dean Norris as his DEA agent brother-in-law Hank Schrader, Bob Odenkirk as criminal lawyer Saul Goodman, Giancarlo Esposito as the calculating Gus Fring, and Jonathan Banks as the stoic enforcer Mike Ehrmantraut.\n\n## A Perfectly Constructed Narrative\n\nBreaking Bad's five-season arc is a masterclass in storytelling. The series meticulously charts Walter's moral degradation, famously described by Gilligan as \"turning Mr. Chips into Scarface.\" Each season raises the stakes:\n\n- **Season 1**: Walter enters the drug world and confronts his first adversaries\n- **Season 2**: The partnership with Jesse deepens as complications arise\n- **Season 3**: The introduction of Gus Fring creates new dangers\n- **Season 4**: Walt and Gus engage in a chess-like battle of wits\n- **Season A**: All secrets are revealed in a devastating conclusion\n\n## Critical Acclaim\n\nBreaking Bad received universal acclaim, winning 16 Emmy Awards and being named one of the greatest television series of all time by critics. The show's final season achieved a rare 99/100 on Metacritic, with the finale watched by over 10 million viewers.\n\nThe series' impact extended beyond its run, spawning the prequel series Better Call Saul and the sequel film El Camino, both continuing the high quality of storytelling established in the original series.\n\n## Cultural Legacy\n\nBreaking Bad revolutionized television with its cinematic quality, complex character development, and unflinching examination of the American dream gone wrong. Its influence can be seen in countless shows that followed, cementing its place not just as incredible entertainment, but as a profound artistic statement on morality, family, and the consequences of our choices.", + "translation": "", + "headline": "", + "slug": null, + "body": "To quote wikipedia:\n\nMicroblogging is a form of blogging using short posts without titles known as microposts[1][2][3] (or status updates on a minority of websites like Meta Platforms'). Microblogs \"allow users to exchange small elements of content such as short sentences, individual images, or video links\",[1] which may be the major reason for their popularity.[4] Some popular social networks such as Twitter, Threads, Mastodon, Tumblr, Koo, and Instagram can be viewed as collections of microblogs.", "publishDate": null, "featuredImage": { - "imageUrl": "https://m.media-amazon.com/images/M/MV5BYmQ4YWMxYjUtNjZmYi00MDQ1LWFjMjMtNjA5ZDdiYjdiODU5XkEyXkFqcGdeQXVyMTMzNDExODE5._V1_.jpg", - "credit": "AMC", - "caption": "Official Breaking Bad series poster", - "altText": "Breaking Bad poster featuring Walter White (Bryan Cranston) and Jesse Pinkman (Aaron Paul) with the show's logo", + "imageUrl": null, + "credit": null, + "caption": null, + "altText": null, "size": "actual", - "height": 1500, - "width": 1000 + "height": null, + "width": null }, "description": null, "thumbnailURL": null }, "relationships": { - "authors": { + "authorBio": { "links": { "self": null } @@ -29,16 +29,6 @@ "links": { "self": null } - }, - "categories": { - "links": { - "self": null - } - }, - "editors": { - "links": { - "self": null - } } }, "meta": {