Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Avoid unnecessary fetching of messages and posts #7357

Merged
merged 12 commits into from
Oct 13, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class CourseDiscussionComponent extends CourseDiscussionDirective impleme
});
});
this.postsSubscription = this.metisService.posts.pipe().subscribe((posts: Post[]) => {
this.posts = posts;
this.posts = posts.slice();
this.isLoading = false;
});
this.totalItemsSubscription = this.metisService.totalNumberOfPosts.pipe().subscribe((totalItems: number) => {
Expand Down
68 changes: 32 additions & 36 deletions src/main/webapp/app/shared/metis/metis.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,50 +475,46 @@ export class MetisService implements OnDestroy {
answer.creationDate = dayjs(answer.creationDate);
});

if (
(postDTO.post.courseWideContext && this.currentPostContextFilter.courseWideContexts?.includes(postDTO.post.courseWideContext)) ||
(postDTO.post.lecture?.id !== undefined && this.currentPostContextFilter.lectureIds?.includes(postDTO.post.lecture.id)) ||
(postDTO.post.exercise?.id !== undefined && this.currentPostContextFilter.exerciseIds?.includes(postDTO.post.exercise.id)) ||
(postDTO.post.conversation?.id !== undefined && postDTO.post.conversation.id === this.currentPostContextFilter.conversationId)
)
switch (postDTO.action) {
case MetisPostAction.CREATE:
// we can add the sent post to the cached posts without violating the current context filter setting
this.cachedPosts.push(postDTO.post);
this.addTags(postDTO.post.tags);
break;
case MetisPostAction.UPDATE:
const indexToUpdate = this.cachedPosts.findIndex((post) => post.id === postDTO.post.id);
if (indexToUpdate > -1) {
this.cachedPosts[indexToUpdate] = postDTO.post;
} else {
console.error(`Post with id ${postDTO.post.id} could not be updated`);
}
this.addTags(postDTO.post.tags);
break;
case MetisPostAction.DELETE:
const indexToDelete = this.cachedPosts.findIndex((post) => post.id === postDTO.post.id);
if (indexToDelete > -1) {
this.cachedPosts.splice(indexToDelete, 1);
} else {
console.error(`Post with id ${postDTO.post.id} could not be deleted`);
}
break;
default:
break;
}
// emit updated version of cachedPosts to subscribing components
switch (postDTO.action) {
case MetisPostAction.CREATE:
if (
postDTO.post.conversation?.id !== undefined &&
postDTO.post.conversation.id === this.currentPostContextFilter.conversationId &&
(!this.currentPostContextFilter.searchText || postDTO.post.content?.toLowerCase().includes(this.currentPostContextFilter.searchText.toLowerCase()))
) {
// we can add the received conversation message to the cached messages without violating the current context filter setting
this.cachedPosts = [postDTO.post, ...this.cachedPosts];
}
this.addTags(postDTO.post.tags);
break;
case MetisPostAction.UPDATE:
const indexToUpdate = this.cachedPosts.findIndex((post) => post.id === postDTO.post.id);
if (indexToUpdate > -1) {
this.cachedPosts[indexToUpdate] = postDTO.post;
}
this.addTags(postDTO.post.tags);
break;
case MetisPostAction.DELETE:
const indexToDelete = this.cachedPosts.findIndex((post) => post.id === postDTO.post.id);
if (indexToDelete > -1) {
this.cachedPosts.splice(indexToDelete, 1);
}
break;
default:
break;
}
// emit updated version of cachedPosts to subscribing components...
if (PageType.OVERVIEW === this.pageType) {
// by invoking the getFilteredPosts method with forceUpdate set to true, i.e. refetch currently displayed posts from server
const oldPage = this.currentPostContextFilter.page;
const oldPageSize = this.currentPostContextFilter.pageSize;
this.currentPostContextFilter.pageSize = oldPageSize! * (oldPage! + 1);
this.currentPostContextFilter.page = 0;
this.getFilteredPosts(this.currentPostContextFilter, true, this.currentConversation);
// ...by invoking the getFilteredPosts method with forceUpdate set to true iff receiving a new Q&A post, i.e. fetching posts from server only in this case
this.getFilteredPosts(this.currentPostContextFilter, !postDTO.post.conversation && postDTO.action === MetisPostAction.CREATE, this.currentConversation);
this.currentPostContextFilter.pageSize = oldPageSize;
this.currentPostContextFilter.page = oldPage;
} else {
// by invoking the getFilteredPosts method with forceUpdate set to false, i.e. without fetching posts from server
// ...by invoking the getFilteredPosts method with forceUpdate set to false, i.e. without fetching posts from server
this.getFilteredPosts(this.currentPostContextFilter, false);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/main/webapp/content/scss/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ Course Info Bar
box-shadow: 3px 3px 6px $mat-autocomplete-visible-box-shadow;
}

.mat-select-panel {
.cdk-overlay-container .cdk-overlay-pane .mat-select-panel {
background: $metis-course-discussion-select-bg;
-webkit-box-shadow: 3px 3px 6px $mat-autocomplete-visible-box-shadow;
-moz-box-shadow: 3px 3px 6px $mat-autocomplete-visible-box-shadow;
Expand Down
104 changes: 102 additions & 2 deletions src/test/javascript/spec/service/metis/metis.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { AnswerPost } from 'app/entities/metis/answer-post.model';
import { ReactionService } from 'app/shared/metis/reaction.service';
import { MockReactionService } from '../../helpers/mocks/service/mock-reaction.service';
import { Reaction } from 'app/entities/metis/reaction.model';
import { CourseWideContext, DisplayPriority, MetisPostAction, MetisWebsocketChannelPrefix, PageType } from 'app/shared/metis/metis.util';
import { CourseWideContext, DisplayPriority, MetisPostAction, MetisWebsocketChannelPrefix, PageType, PostContextFilter } from 'app/shared/metis/metis.util';
import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service';
import { TranslateService } from '@ngx-translate/core';
import { Router } from '@angular/router';
Expand All @@ -23,7 +23,7 @@ import { LocalStorageService, SessionStorageService } from 'ngx-webstorage';
import { MockProvider } from 'ng-mocks';
import { JhiWebsocketService } from 'app/core/websocket/websocket.service';
import { MetisPostDTO } from 'app/entities/metis/metis-post-dto.model';
import { of } from 'rxjs';
import { Subject, of } from 'rxjs';
import {
metisCourse,
metisCoursePostsWithCourseWideContext,
Expand All @@ -42,6 +42,7 @@ import {
import { ITEMS_PER_PAGE } from 'app/shared/constants/pagination.constants';
import { ChannelDTO, ChannelSubType } from 'app/entities/metis/conversation/channel.model';
import { ConversationType } from 'app/entities/metis/conversation/conversation.model';
import { HttpHeaders, HttpResponse } from '@angular/common/http';

describe('Metis Service', () => {
let metisService: MetisService;
Expand Down Expand Up @@ -549,5 +550,104 @@ describe('Metis Service', () => {
isCourseWide: false,
} as ChannelDTO);
}));

it.each([MetisPostAction.CREATE, MetisPostAction.UPDATE, MetisPostAction.DELETE])(
'should not call postService.getPosts() for new or updated messages received over WebSocket',
(action: MetisPostAction) => {
// Setup
const channel = 'someChannel';
const mockPostDTO = {
post: metisPostInChannel,
action,
};
const mockReceiveObservable = new Subject();
websocketServiceReceiveStub.mockReturnValue(mockReceiveObservable.asObservable());
metisService.setPageType(PageType.OVERVIEW);
metisService.createWebsocketSubscription(channel);

// set currentPostContextFilter appropriately
metisService.getFilteredPosts({ conversationId: mockPostDTO.post.conversation?.id } as PostContextFilter);

// Ensure subscribe to websocket was called
expect(websocketService.subscribe).toHaveBeenCalled();

// Emulate receiving a message
const getPostsSpy = jest.spyOn(postService, 'getPosts');
mockReceiveObservable.next(mockPostDTO);

// Ensure getPosts() was not called
expect(getPostsSpy).not.toHaveBeenCalled();
},
);

it('should update displayed conversation messages if new message does not match search text', fakeAsync(() => {
// Setup
const channel = 'someChannel';
const mockPostDTO = {
post: { ...metisPostInChannel, content: 'search Text' },
action: MetisPostAction.CREATE,
};
const mockReceiveObservable = new Subject();
websocketServiceReceiveStub.mockReturnValue(mockReceiveObservable.asObservable());
metisService.setPageType(PageType.OVERVIEW);
metisService.createWebsocketSubscription(channel);

jest.spyOn(postService, 'getPosts').mockReturnValue(
of(
new HttpResponse({
body: [],
headers: new HttpHeaders({
'X-Total-Count': 0,
}),
}),
),
);

// set currentPostContextFilter with search text
metisService.getFilteredPosts({ conversationId: mockPostDTO.post.conversation?.id, searchText: 'Search text' } as PostContextFilter);

// Emulate receiving a message matching the search text
mockReceiveObservable.next(mockPostDTO);
// Emulate receiving a message not matching the search text
mockReceiveObservable.next({
post: { ...metisPostInChannel, content: 'other Text' },
action: MetisPostAction.CREATE,
});

metisService.posts.subscribe((posts) => {
expect(posts[0]).toBe(mockPostDTO.post);
});
tick();
}));

it.each([MetisPostAction.CREATE, MetisPostAction.UPDATE, MetisPostAction.DELETE])(
'should not call postService.getPosts() for new or updated messages received over WebSocket',
(action: MetisPostAction) => {
// Setup
const channel = 'someChannel';
const mockPostDTO = {
post: metisLecturePosts[0],
action,
};
const mockReceiveObservable = new Subject();
websocketServiceReceiveStub.mockReturnValue(mockReceiveObservable.asObservable());
const getPostsSpy = jest.spyOn(postService, 'getPosts');
metisService.setPageType(PageType.OVERVIEW);
metisService.createWebsocketSubscription(channel);

// Ensure subscribe to websocket was called
expect(websocketService.subscribe).toHaveBeenCalled();

// Emulate receiving a post
mockReceiveObservable.next(mockPostDTO);

// Ensure getPosts() was not called
if (action === MetisPostAction.CREATE) {
expect(getPostsSpy).toHaveBeenCalledOnce();
} else {
expect(getPostsSpy).not.toHaveBeenCalled();
}
},
);
});
});