Skip to content

Commit c8875f8

Browse files
authored
fix: liveSync just starts one sync job at a time (#2534)
1 parent 6cb284a commit c8875f8

File tree

4 files changed

+70
-22
lines changed

4 files changed

+70
-22
lines changed

src/app/child-dev-project/notes/dashboard-widgets/notes-dashboard/notes-dashboard.component.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { ChildrenService } from "../../../children/children.service";
1010
import { NotesDashboardComponent } from "./notes-dashboard.component";
1111
import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
1212
import { TestEntity } from "../../../../utils/test-utils/TestEntity";
13+
import { EntityRegistry } from "../../../../core/entity/database-entity.decorator";
14+
import { Entity } from "../../../../core/entity/model/entity";
15+
16+
class Child extends Entity {
17+
static ENTITY_TYPE = "Child";
18+
}
1319

1420
describe("NotesDashboardComponent", () => {
1521
let component: NotesDashboardComponent;
@@ -29,6 +35,8 @@ describe("NotesDashboardComponent", () => {
2935
imports: [NotesDashboardComponent, MockedTestingModule.withState()],
3036
providers: [{ provide: ChildrenService, useValue: mockChildrenService }],
3137
}).compileComponents();
38+
39+
TestBed.inject(EntityRegistry).set("Child", Child);
3240
}));
3341

3442
describe("with recent notes", () => {

src/app/core/admin/admin-entity-details/admin-entity-form/admin-entity-form.component.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import { CoreTestingModule } from "../../../../utils/core-testing.module";
1010
import { EntityFormService } from "../../../common-components/entity-form/entity-form.service";
1111
import { MatDialog } from "@angular/material/dialog";
1212
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
13-
import { Note } from "../../../../child-dev-project/notes/model/note";
1413
import { FormGroup } from "@angular/forms";
1514
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
1615
import { CdkDragDrop } from "@angular/cdk/drag-drop";
1716
import { of } from "rxjs";
1817
import { AdminModule } from "../../admin.module";
1918
import { FormConfig } from "../../../entity-details/form/form.component";
2019
import { ColumnConfig } from "../../../common-components/entity-form/FormConfig";
20+
import { TestEntity } from "../../../../utils/test-utils/TestEntity";
2121

2222
describe("AdminEntityFormComponent", () => {
2323
let component: AdminEntityFormComponent;
@@ -31,7 +31,7 @@ describe("AdminEntityFormComponent", () => {
3131
beforeEach(() => {
3232
testConfig = {
3333
fieldGroups: [
34-
{ header: "Group 1", fields: ["subject", "date"] },
34+
{ header: "Group 1", fields: ["name", "other"] },
3535
{ fields: ["category"] },
3636
],
3737
};
@@ -64,7 +64,7 @@ describe("AdminEntityFormComponent", () => {
6464
component = fixture.componentInstance;
6565

6666
component.config = testConfig;
67-
component.entityType = Note;
67+
component.entityType = TestEntity;
6868

6969
fixture.detectChanges();
7070

@@ -85,7 +85,7 @@ describe("AdminEntityFormComponent", () => {
8585
};
8686
component.ngOnChanges({ config: true as any });
8787

88-
const noteUserFacingFields = Array.from(Note.schema.entries())
88+
const noteUserFacingFields = Array.from(TestEntity.schema.entries())
8989
.filter(([key, value]) => value.label)
9090
.sort(([aId, a], [bId, b]) => a.label.localeCompare(b.label))
9191
.map(([key]) => key);
@@ -126,7 +126,7 @@ describe("AdminEntityFormComponent", () => {
126126
tick();
127127

128128
expect(mockDialog.open).toHaveBeenCalled();
129-
expect(targetContainer).toEqual(["subject", newFieldId, "date"]);
129+
expect(targetContainer).toEqual(["name", newFieldId, "other"]);
130130
expect(component.availableFields).toContain(
131131
component.createNewFieldPlaceholder,
132132
);
@@ -139,7 +139,7 @@ describe("AdminEntityFormComponent", () => {
139139
component.drop(mockDropNewFieldEvent(targetContainer));
140140
tick();
141141

142-
expect(targetContainer).toEqual(["subject", "date"]);
142+
expect(targetContainer).toEqual(["name", "other"]);
143143
expect(mockDialog.open).toHaveBeenCalled();
144144
expect(component.availableFields).toContain(
145145
component.createNewFieldPlaceholder,

src/app/core/database/sync.service.spec.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import { LoginState } from "../session/session-states/login-state.enum";
88
import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
99
import { Subject } from "rxjs";
1010
import { NAVIGATOR_TOKEN } from "../../utils/di-tokens";
11+
import { SyncState } from "../session/session-states/sync-state.enum";
1112

1213
describe("SyncService", () => {
1314
let service: SyncService;
1415
let loginState: LoginStateSubject;
1516
let mockAuthService: jasmine.SpyObj<KeycloakAuthService>;
1617
let mockNavigator;
1718

19+
let mockSyncStateSubject: SyncStateSubject;
20+
1821
beforeEach(() => {
1922
mockAuthService = jasmine.createSpyObj(["login", "addAuthHeader"]);
2023
mockNavigator = { onLine: true };
@@ -30,6 +33,7 @@ describe("SyncService", () => {
3033
});
3134
service = TestBed.inject(SyncService);
3235
loginState = TestBed.inject(LoginStateSubject);
36+
mockSyncStateSubject = TestBed.inject(SyncStateSubject);
3337
});
3438

3539
/**
@@ -45,18 +49,29 @@ describe("SyncService", () => {
4549
expect(service).toBeTruthy();
4650
});
4751

48-
it("should restart the sync if it fails at one point", fakeAsync(() => {
52+
/**
53+
* Set up a mocked db and localDb for tests and override the TestBed service provider.
54+
*/
55+
function mockPouchDatabaseService(): {
56+
mockLocalDb: jasmine.SpyObj<PouchDB.Database>;
57+
db: PouchDatabase;
58+
} {
59+
mockSyncStateSubject.next(SyncState.UNSYNCED);
4960
const mockLocalDb = jasmine.createSpyObj(["sync"]);
50-
spyOn(
51-
TestBed.inject(Database) as PouchDatabase,
52-
"getPouchDB",
53-
).and.returnValue(mockLocalDb);
61+
mockLocalDb.sync.and.resolveTo({});
62+
63+
const db = TestBed.inject(Database) as PouchDatabase;
64+
spyOn(db, "getPouchDB").and.returnValue(mockLocalDb);
65+
return { mockLocalDb, db };
66+
}
67+
68+
it("should restart the sync if it fails at one point", fakeAsync(() => {
69+
const { mockLocalDb } = mockPouchDatabaseService();
5470

5571
loginState.next(LoginState.LOGGED_IN);
5672

5773
service.startSync();
5874

59-
mockLocalDb.sync.and.resolveTo({});
6075
tick(1000);
6176
expect(mockLocalDb.sync).toHaveBeenCalled();
6277

@@ -76,17 +91,14 @@ describe("SyncService", () => {
7691
}));
7792

7893
it("should sync immediately when local db has changes", fakeAsync(() => {
79-
const mockLocalDb = jasmine.createSpyObj(["sync"]);
80-
const db = TestBed.inject(Database) as PouchDatabase;
81-
spyOn(db, "getPouchDB").and.returnValue(mockLocalDb);
94+
const { mockLocalDb, db } = mockPouchDatabaseService();
8295
const mockChanges = new Subject();
8396
spyOn(db, "changes").and.returnValue(mockChanges);
8497

8598
loginState.next(LoginState.LOGGED_IN);
8699

87100
service.startSync();
88101

89-
mockLocalDb.sync.and.resolveTo({});
90102
tick(1000);
91103
expect(mockLocalDb.sync).toHaveBeenCalled();
92104
mockLocalDb.sync.calls.reset();
@@ -101,10 +113,7 @@ describe("SyncService", () => {
101113
}));
102114

103115
it("should skip sync calls when offline", fakeAsync(() => {
104-
const mockLocalDb = jasmine.createSpyObj(["sync"]);
105-
mockLocalDb.sync.and.resolveTo({});
106-
const db = TestBed.inject(Database) as PouchDatabase;
107-
spyOn(db, "getPouchDB").and.returnValue(mockLocalDb);
116+
const { mockLocalDb } = mockPouchDatabaseService();
108117

109118
mockNavigator.onLine = false;
110119

@@ -119,4 +128,29 @@ describe("SyncService", () => {
119128

120129
stopPeriodicTimer();
121130
}));
131+
132+
it("should not start additional syncs while a previous sync is still running", fakeAsync(() => {
133+
const LONG_SYNC_TIME = 100000;
134+
135+
const { mockLocalDb } = mockPouchDatabaseService();
136+
mockLocalDb.sync.and.callFake(
137+
// @ts-ignore
138+
async () => await new Promise((r) => setTimeout(r, LONG_SYNC_TIME)),
139+
);
140+
141+
service.startSync();
142+
143+
tick(SyncService.SYNC_INTERVAL);
144+
expect(mockLocalDb.sync).toHaveBeenCalledTimes(1);
145+
146+
tick(SyncService.SYNC_INTERVAL);
147+
expect(mockLocalDb.sync).toHaveBeenCalledTimes(1);
148+
149+
tick(LONG_SYNC_TIME);
150+
expect(mockLocalDb.sync).toHaveBeenCalledTimes(2);
151+
152+
// stop periodic timer:
153+
service.liveSyncEnabled = false;
154+
tick(LONG_SYNC_TIME);
155+
}));
122156
});

src/app/core/database/sync.service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
1515
import { Config } from "../config/config";
1616
import { Entity } from "../entity/model/entity";
17-
import { from, interval, merge, of } from "rxjs";
17+
import { from, interval, merge, of, Subscription } from "rxjs";
1818
import { environment } from "../../../environments/environment";
1919
import { NAVIGATOR_TOKEN } from "../../utils/di-tokens";
2020

@@ -132,7 +132,13 @@ export class SyncService {
132132
)
133133
.pipe(
134134
debounceTime(500),
135-
mergeMap(() => from(this.sync())),
135+
mergeMap(() => {
136+
if (this.syncStateSubject.value == SyncState.STARTED) {
137+
return of();
138+
} else {
139+
return from(this.sync());
140+
}
141+
}),
136142
retry({ delay: SyncService.SYNC_INTERVAL }),
137143
takeWhile(() => this.liveSyncEnabled),
138144
)

0 commit comments

Comments
 (0)