Skip to content

Commit 7842d29

Browse files
committed
Dispose realtime docs when no longer in use
1 parent 1272235 commit 7842d29

File tree

123 files changed

+2127
-904
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+2127
-904
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ permissions: {}
33

44
on:
55
pull_request:
6-
branches:
7-
- "e2e/**"
86
merge_group:
97
workflow_dispatch:
108
schedule:
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
level: error
3+
---
4+
5+
# No misused mock patterns outside of mocks
6+
7+
`something.something(anything())` is wrong unless it's inside a `when` or `verify` call.
8+
9+
```grit
10+
`$functionCall()` where $functionCall <: and {
11+
or {
12+
`anyOfClass`,
13+
`anyFunction`,
14+
`anyNumber`,
15+
`anyString`,
16+
`anything`
17+
},
18+
and {
19+
not within `when($_)`,
20+
not within `verify($_)`,
21+
}
22+
}
23+
24+
```
25+
26+
## Positive example
27+
28+
```ts
29+
console.log(anything());
30+
```
31+
32+
```ts
33+
console.log(anything());
34+
```
35+
36+
## Negative example using when()
37+
38+
```ts
39+
when(something.something(anything())).thenReturn(somethingElse);
40+
```
41+
42+
## Negative example using verify()
43+
44+
```ts
45+
verify(something.something(anything())).once();
46+
```

src/SIL.XForge.Scripture/ClientApp/e2e/workflows/community-checking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ async function joinAsChecker(
174174
// Give time for the last answer to be saved
175175
await page.waitForTimeout(500);
176176
} catch (e) {
177+
await page.pause();
177178
console.error('Error running tests for checker ' + userNumber);
178179
console.error(e);
179180
await screenshot(

src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ServalAdminAuthGuard } from './serval-administration/serval-admin-auth.
1414
import { ServalAdministrationComponent } from './serval-administration/serval-administration.component';
1515
import { ServalProjectComponent } from './serval-administration/serval-project.component';
1616
import { SettingsComponent } from './settings/settings.component';
17+
import { BlankPageComponent } from './shared/blank-page/blank-page.component';
1718
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
1819
import { SettingsAuthGuard, SyncAuthGuard } from './shared/project-router.guard';
1920
import { SyncComponent } from './sync/sync.component';
@@ -33,6 +34,7 @@ const routes: Routes = [
3334
{ path: 'serval-administration/:projectId', component: ServalProjectComponent, canActivate: [ServalAdminAuthGuard] },
3435
{ path: 'serval-administration', component: ServalAdministrationComponent, canActivate: [ServalAdminAuthGuard] },
3536
{ path: 'system-administration', component: SystemAdministrationComponent, canActivate: [SystemAdminAuthGuard] },
37+
{ path: 'blank-page', component: BlankPageComponent },
3638
{ path: '**', component: PageNotFoundComponent }
3739
];
3840

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ExternalUrlService } from 'xforge-common/external-url.service';
2525
import { FileService } from 'xforge-common/file.service';
2626
import { I18nService } from 'xforge-common/i18n.service';
2727
import { LocationService } from 'xforge-common/location.service';
28+
import { DocSubscription } from 'xforge-common/models/realtime-doc';
2829
import { UserDoc } from 'xforge-common/models/user-doc';
2930
import { NoticeService } from 'xforge-common/notice.service';
3031
import { OnlineStatusService } from 'xforge-common/online-status.service';
@@ -678,11 +679,12 @@ class TestEnvironment {
678679
this.addProjectUserConfig('project01', 'user03');
679680
this.addProjectUserConfig('project01', 'user04');
680681

681-
when(mockedSFProjectService.getProfile(anything())).thenCall(projectId =>
682-
this.realtimeService.subscribe(SFProjectProfileDoc.COLLECTION, projectId)
682+
when(mockedSFProjectService.getProfile(anything(), anything())).thenCall((projectId, subscription) =>
683+
this.realtimeService.subscribe(SFProjectProfileDoc.COLLECTION, projectId, subscription)
683684
);
684-
when(mockedSFProjectService.getUserConfig(anything(), anything())).thenCall((projectId, userId) =>
685-
this.realtimeService.subscribe(SFProjectUserConfigDoc.COLLECTION, `${projectId}:${userId}`)
685+
when(mockedSFProjectService.getUserConfig(anything(), anything(), anything())).thenCall(
686+
(projectId, userId, subscriber) =>
687+
this.realtimeService.subscribe(SFProjectUserConfigDoc.COLLECTION, `${projectId}:${userId}`, subscriber)
686688
);
687689
when(mockedLocationService.pathname).thenReturn('/projects/project01/checking');
688690

@@ -792,12 +794,14 @@ class TestEnvironment {
792794
}
793795

794796
get currentUserDoc(): UserDoc {
795-
return this.realtimeService.get(UserDoc.COLLECTION, 'user01');
797+
return this.realtimeService.get(UserDoc.COLLECTION, 'user01', new DocSubscription('spec'));
796798
}
797799

798800
setCurrentUser(userId: string): void {
799801
when(mockedUserService.currentUserId).thenReturn(userId);
800-
when(mockedUserService.getCurrentUser()).thenCall(() => this.realtimeService.subscribe(UserDoc.COLLECTION, userId));
802+
when(mockedUserService.getCurrentUser()).thenCall(() =>
803+
this.realtimeService.subscribe(UserDoc.COLLECTION, userId, new DocSubscription('spec'))
804+
);
801805
}
802806

803807
triggerLogin(): void {
@@ -865,33 +869,53 @@ class TestEnvironment {
865869
when(mockedUserService.currentProjectId(anything())).thenReturn(undefined);
866870
}
867871
this.ngZone.run(() => {
868-
const projectDoc = this.realtimeService.get(SFProjectProfileDoc.COLLECTION, projectId);
872+
const projectDoc = this.realtimeService.get(
873+
SFProjectProfileDoc.COLLECTION,
874+
projectId,
875+
new DocSubscription('spec')
876+
);
869877
projectDoc.delete();
870878
});
871879
this.wait();
872880
}
873881

874882
removeUserFromProject(projectId: string): void {
875-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, projectId);
883+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
884+
SFProjectProfileDoc.COLLECTION,
885+
projectId,
886+
new DocSubscription('spec')
887+
);
876888
projectDoc.submitJson0Op(op => op.unset<string>(p => p.userRoles['user01']), false);
877889
this.wait();
878890
}
879891

880892
updatePreTranslate(projectId: string): void {
881-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, projectId);
893+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
894+
SFProjectProfileDoc.COLLECTION,
895+
projectId,
896+
new DocSubscription('spec')
897+
);
882898
projectDoc.submitJson0Op(op => op.set<boolean>(p => p.translateConfig.preTranslate, true), false);
883899
this.wait();
884900
}
885901

886902
addUserToProject(projectId: string): void {
887-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, projectId);
903+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
904+
SFProjectProfileDoc.COLLECTION,
905+
projectId,
906+
new DocSubscription('spec')
907+
);
888908
projectDoc.submitJson0Op(op => op.set<string>(p => p.userRoles['user01'], SFProjectRole.CommunityChecker), false);
889909
this.currentUserDoc.submitJson0Op(op => op.add<string>(u => u.sites['sf'].projects, 'project04'), false);
890910
this.wait();
891911
}
892912

893913
changeUserRole(projectId: string, userId: string, role: SFProjectRole): void {
894-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, projectId);
914+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
915+
SFProjectProfileDoc.COLLECTION,
916+
projectId,
917+
new DocSubscription('spec')
918+
);
895919
projectDoc.submitJson0Op(op => op.set<string>(p => p.userRoles[userId], role), false);
896920
this.wait();
897921
}

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { FileService } from 'xforge-common/file.service';
2424
import { I18nService } from 'xforge-common/i18n.service';
2525
import { LocationService } from 'xforge-common/location.service';
2626
import { Breakpoint, MediaBreakpointService } from 'xforge-common/media-breakpoints/media-breakpoint.service';
27+
import { DocSubscription } from 'xforge-common/models/realtime-doc';
2728
import { UserDoc } from 'xforge-common/models/user-doc';
2829
import { NoticeService } from 'xforge-common/notice.service';
2930
import { OnlineStatusService } from 'xforge-common/online-status.service';
@@ -282,7 +283,8 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
282283
this.userService.setCurrentProjectId(this.currentUserDoc!, this._selectedProjectDoc.id);
283284
this.projectUserConfigDoc = await this.projectService.getUserConfig(
284285
this._selectedProjectDoc.id,
285-
this.currentUserDoc!.id
286+
this.currentUserDoc!.id,
287+
new DocSubscription('AppComponent', this.destroyRef)
286288
);
287289
if (this.selectedProjectDeleteSub != null) {
288290
this.selectedProjectDeleteSub.unsubscribe();

src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ProjectComponent } from './project/project.component';
3737
import { ScriptureChooserDialogComponent } from './scripture-chooser-dialog/scripture-chooser-dialog.component';
3838
import { DeleteProjectDialogComponent } from './settings/delete-project-dialog/delete-project-dialog.component';
3939
import { SettingsComponent } from './settings/settings.component';
40+
import { CacheService } from './shared/cache-service/cache.service';
4041
import { GlobalNoticesComponent } from './shared/global-notices/global-notices.component';
4142
import { SharedModule } from './shared/shared.module';
4243
import { TextNoteDialogComponent } from './shared/text/text-note-dialog/text-note-dialog.component';
@@ -46,6 +47,11 @@ import { LynxInsightsModule } from './translate/editor/lynx/insights/lynx-insigh
4647
import { TranslateModule } from './translate/translate.module';
4748
import { UsersModule } from './users/users.module';
4849

50+
/** Initialization function for any services that need to be run but are not depended on by any component. */
51+
function initializeGlobalServicesFactory(_cacheService: CacheService): () => Promise<any> {
52+
return () => Promise.resolve();
53+
}
54+
4955
@NgModule({
5056
declarations: [
5157
AppComponent,
@@ -97,6 +103,12 @@ import { UsersModule } from './users/users.module';
97103
{ provide: ErrorHandler, useClass: ExceptionHandlingService },
98104
{ provide: OverlayContainer, useClass: InAppRootOverlayContainer },
99105
provideHttpClient(withInterceptorsFromDi()),
106+
{
107+
provide: APP_INITIALIZER,
108+
useFactory: initializeGlobalServicesFactory,
109+
deps: [CacheService],
110+
multi: true
111+
},
100112
{
101113
provide: APP_INITIALIZER,
102114
useFactory: preloadEnglishTranslations,

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.spec.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { VerseRefData } from 'realtime-server/lib/esm/scriptureforge/models/vers
2929
import { of } from 'rxjs';
3030
import { anything, mock, resetCalls, verify, when } from 'ts-mockito';
3131
import { DialogService } from 'xforge-common/dialog.service';
32+
import { DocSubscription } from 'xforge-common/models/realtime-doc';
3233
import { NoticeService } from 'xforge-common/notice.service';
3334
import { OnlineStatusService } from 'xforge-common/online-status.service';
3435
import { noopDestroyRef } from 'xforge-common/realtime.service';
@@ -420,7 +421,8 @@ describe('CheckingOverviewComponent', () => {
420421
const env = new TestEnvironment();
421422
const questionDoc: QuestionDoc = env.realtimeService.get(
422423
QuestionDoc.COLLECTION,
423-
getQuestionDocId('project01', 'q7Id')
424+
getQuestionDocId('project01', 'q7Id'),
425+
new DocSubscription('spec')
424426
);
425427
await questionDoc.submitJson0Op(op => {
426428
op.set(d => d.isArchived, false);
@@ -973,18 +975,26 @@ class TestEnvironment {
973975
when(mockedActivatedRoute.params).thenReturn(of({ projectId: 'project01' }));
974976
when(mockedQuestionDialogService.questionDialog(anything())).thenResolve();
975977
when(mockedDialogService.confirm(anything(), anything())).thenResolve(true);
976-
when(mockedProjectService.getProfile(anything())).thenCall(id =>
977-
this.realtimeService.subscribe(SFProjectProfileDoc.COLLECTION, id)
978+
when(mockedProjectService.getProfile(anything(), anything())).thenCall((id, subscription) =>
979+
this.realtimeService.subscribe(SFProjectProfileDoc.COLLECTION, id, subscription)
978980
);
979-
when(mockedProjectService.getUserConfig(anything(), anything())).thenCall((id, userId) =>
980-
this.realtimeService.subscribe(SFProjectUserConfigDoc.COLLECTION, getSFProjectUserConfigDocId(id, userId))
981+
when(mockedProjectService.getUserConfig(anything(), anything(), anything())).thenCall((id, userId, subscriber) =>
982+
this.realtimeService.subscribe(
983+
SFProjectUserConfigDoc.COLLECTION,
984+
getSFProjectUserConfigDocId(id, userId),
985+
subscriber
986+
)
981987
);
982988
when(mockedQuestionsService.queryQuestions('project01', anything(), anything())).thenCall(() =>
983989
this.realtimeService.subscribeQuery(QuestionDoc.COLLECTION, {}, noopDestroyRef)
984990
);
985991
when(mockedProjectService.onlineDeleteAudioTimingData(anything(), anything(), anything())).thenCall(
986992
(projectId, book, chapter) => {
987-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, projectId);
993+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
994+
SFProjectProfileDoc.COLLECTION,
995+
projectId,
996+
new DocSubscription('spec')
997+
);
988998
const textIndex: number = projectDoc.data!.texts.findIndex(t => t.bookNum === book);
989999
const chapterIndex: number = projectDoc.data!.texts[textIndex].chapters.findIndex(c => c.number === chapter);
9901000
projectDoc.submitJson0Op(op => op.set(p => p.texts[textIndex].chapters[chapterIndex].hasAudio, false), false);
@@ -1154,7 +1164,11 @@ class TestEnvironment {
11541164
}
11551165

11561166
setSeeOtherUserResponses(isEnabled: boolean): void {
1157-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, 'project01');
1167+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
1168+
SFProjectProfileDoc.COLLECTION,
1169+
'project01',
1170+
new DocSubscription('spec')
1171+
);
11581172
projectDoc.submitJson0Op(
11591173
op => op.set<boolean>(p => p.checkingConfig.usersSeeEachOthersResponses, isEnabled),
11601174
false
@@ -1164,7 +1178,11 @@ class TestEnvironment {
11641178

11651179
setCheckingEnabled(isEnabled: boolean): void {
11661180
this.ngZone.run(() => {
1167-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, 'project01');
1181+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
1182+
SFProjectProfileDoc.COLLECTION,
1183+
'project01',
1184+
new DocSubscription('spec')
1185+
);
11681186
projectDoc.submitJson0Op(op => op.set<boolean>(p => p.checkingConfig.checkingEnabled, isEnabled), false);
11691187
});
11701188
this.waitForProjectDocChanges();
@@ -1224,7 +1242,11 @@ class TestEnvironment {
12241242
],
12251243
permissions: {}
12261244
};
1227-
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(SFProjectProfileDoc.COLLECTION, 'project01');
1245+
const projectDoc = this.realtimeService.get<SFProjectProfileDoc>(
1246+
SFProjectProfileDoc.COLLECTION,
1247+
'project01',
1248+
new DocSubscription('spec')
1249+
);
12281250
const index: number = projectDoc.data!.texts.length - 1;
12291251
projectDoc.submitJson0Op(op => op.insert(p => p.texts, index, text), false);
12301252
this.addQuestion({

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-overview/checking-overview.component.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { DataLoadingComponent } from 'xforge-common/data-loading-component';
1313
import { DialogService } from 'xforge-common/dialog.service';
1414
import { I18nService } from 'xforge-common/i18n.service';
1515
import { L10nNumberPipe } from 'xforge-common/l10n-number.pipe';
16+
import { DocSubscription } from 'xforge-common/models/realtime-doc';
1617
import { RealtimeQuery } from 'xforge-common/models/realtime-query';
1718
import { NoticeService } from 'xforge-common/notice.service';
1819
import { OnlineStatusService } from 'xforge-common/online-status.service';
@@ -181,7 +182,10 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O
181182
const projectId$ = this.activatedRoute.params.pipe(
182183
tap(params => {
183184
this.loadingStarted();
184-
projectDocPromise = this.projectService.getProfile(params['projectId']);
185+
projectDocPromise = this.projectService.getProfile(
186+
params['projectId'],
187+
new DocSubscription('CheckingOverviewComponent', this.destroyRef)
188+
);
185189
}),
186190
map(params => params['projectId'] as string)
187191
);
@@ -190,7 +194,11 @@ export class CheckingOverviewComponent extends DataLoadingComponent implements O
190194
this.projectId = projectId;
191195
try {
192196
this.projectDoc = await projectDocPromise;
193-
this.projectUserConfigDoc = await this.projectService.getUserConfig(projectId, this.userService.currentUserId);
197+
this.projectUserConfigDoc = await this.projectService.getUserConfig(
198+
projectId,
199+
this.userService.currentUserId,
200+
new DocSubscription('CheckingOverviewComponent', this.destroyRef)
201+
);
194202
this.questionsQuery?.dispose();
195203
this.questionsQuery = await this.checkingQuestionsService.queryQuestions(
196204
projectId,

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-comments/checking-comments.stories.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { expect, within } from '@storybook/test';
44
import { createTestUserProfile } from 'realtime-server/lib/esm/common/models/user-test-data';
55
import { Comment } from 'realtime-server/lib/esm/scriptureforge/models/comment';
66
import { createTestProject } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data';
7-
import { instance, mock, when } from 'ts-mockito';
7+
import { anything, instance, mock, when } from 'ts-mockito';
88
import { DialogService } from 'xforge-common/dialog.service';
99
import { I18nStoryModule } from 'xforge-common/i18n-story.module';
1010
import { UserProfileDoc } from 'xforge-common/models/user-profile-doc';
@@ -17,11 +17,11 @@ import { CheckingCommentsComponent } from './checking-comments.component';
1717
const mockedDialogService = mock(DialogService);
1818
const mockedUserService = mock(UserService);
1919
when(mockedUserService.currentUserId).thenReturn('user01');
20-
when(mockedUserService.getProfile('user01')).thenResolve({
20+
when(mockedUserService.getProfile('user01', anything())).thenResolve({
2121
id: 'user01',
2222
data: createTestUserProfile({}, 1)
2323
} as UserProfileDoc);
24-
when(mockedUserService.getProfile('user02')).thenResolve({
24+
when(mockedUserService.getProfile('user02', anything())).thenResolve({
2525
id: 'user02',
2626
data: createTestUserProfile({}, 2)
2727
} as UserProfileDoc);

0 commit comments

Comments
 (0)