mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-21090] Vault - Repeated Syncs (#14740)
* move `fullSync` contents to private methods in prep to storing the respective promise * store in-flight sync so multiple calls to the sync service are avoided * Revert "store in-flight sync so multiple calls to the sync service are avoided" This reverts commit233c8e9d4b. * Revert "move `fullSync` contents to private methods in prep to storing the respective promise" This reverts commit3f686ac6a4. * store inflight API calls for sync service - This avoids duplicate network requests in a relatively short amount of time but still allows consumers to call `fullSync` if needed * add debug log for duplicate sync
This commit is contained in:
@@ -130,23 +130,23 @@ describe("DefaultSyncService", () => {
|
|||||||
|
|
||||||
const user1 = "user1" as UserId;
|
const user1 = "user1" as UserId;
|
||||||
|
|
||||||
|
const emptySyncResponse = new SyncResponse({
|
||||||
|
profile: {
|
||||||
|
id: user1,
|
||||||
|
},
|
||||||
|
folders: [],
|
||||||
|
collections: [],
|
||||||
|
ciphers: [],
|
||||||
|
sends: [],
|
||||||
|
domains: [],
|
||||||
|
policies: [],
|
||||||
|
});
|
||||||
|
|
||||||
describe("fullSync", () => {
|
describe("fullSync", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountService.activeAccount$ = of({ id: user1 } as Account);
|
accountService.activeAccount$ = of({ id: user1 } as Account);
|
||||||
Matrix.autoMockMethod(authService.authStatusFor$, () => of(AuthenticationStatus.Unlocked));
|
Matrix.autoMockMethod(authService.authStatusFor$, () => of(AuthenticationStatus.Unlocked));
|
||||||
apiService.getSync.mockResolvedValue(
|
apiService.getSync.mockResolvedValue(emptySyncResponse);
|
||||||
new SyncResponse({
|
|
||||||
profile: {
|
|
||||||
id: user1,
|
|
||||||
},
|
|
||||||
folders: [],
|
|
||||||
collections: [],
|
|
||||||
ciphers: [],
|
|
||||||
sends: [],
|
|
||||||
domains: [],
|
|
||||||
policies: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
Matrix.autoMockMethod(userDecryptionOptionsService.userDecryptionOptionsById$, () =>
|
Matrix.autoMockMethod(userDecryptionOptionsService.userDecryptionOptionsById$, () =>
|
||||||
of({ hasMasterPassword: true } satisfies UserDecryptionOptions),
|
of({ hasMasterPassword: true } satisfies UserDecryptionOptions),
|
||||||
);
|
);
|
||||||
@@ -201,5 +201,44 @@ describe("DefaultSyncService", () => {
|
|||||||
expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1);
|
expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1);
|
||||||
expect(apiService.getSync).toHaveBeenCalledTimes(1);
|
expect(apiService.getSync).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("in-flight syncs", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call getSync when one is already in progress", async () => {
|
||||||
|
const fullSyncPromises = [sut.fullSync(true), sut.fullSync(false), sut.fullSync(false)];
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
|
||||||
|
await Promise.all(fullSyncPromises);
|
||||||
|
|
||||||
|
expect(apiService.getSync).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call refreshIdentityToken when one is already in progress", async () => {
|
||||||
|
const fullSyncPromises = [sut.fullSync(true), sut.fullSync(false), sut.fullSync(false)];
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100);
|
||||||
|
|
||||||
|
await Promise.all(fullSyncPromises);
|
||||||
|
|
||||||
|
expect(apiService.refreshIdentityToken).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets the in-flight properties when the complete", async () => {
|
||||||
|
const fullSyncPromises = [sut.fullSync(true), sut.fullSync(true)];
|
||||||
|
|
||||||
|
await Promise.all(fullSyncPromises);
|
||||||
|
|
||||||
|
expect(sut["inFlightApiCalls"].refreshToken).toBeNull();
|
||||||
|
expect(sut["inFlightApiCalls"].sync).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,11 +58,21 @@ import { MessageSender } from "../messaging";
|
|||||||
import { StateProvider } from "../state";
|
import { StateProvider } from "../state";
|
||||||
|
|
||||||
import { CoreSyncService } from "./core-sync.service";
|
import { CoreSyncService } from "./core-sync.service";
|
||||||
|
import { SyncResponse } from "./sync.response";
|
||||||
import { SyncOptions } from "./sync.service";
|
import { SyncOptions } from "./sync.service";
|
||||||
|
|
||||||
export class DefaultSyncService extends CoreSyncService {
|
export class DefaultSyncService extends CoreSyncService {
|
||||||
syncInProgress = false;
|
syncInProgress = false;
|
||||||
|
|
||||||
|
/** The promises associated with any in-flight api calls. */
|
||||||
|
private inFlightApiCalls: {
|
||||||
|
refreshToken: Promise<void> | null;
|
||||||
|
sync: Promise<SyncResponse> | null;
|
||||||
|
} = {
|
||||||
|
refreshToken: null,
|
||||||
|
sync: null,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
@@ -141,9 +151,24 @@ export class DefaultSyncService extends CoreSyncService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!skipTokenRefresh) {
|
if (!skipTokenRefresh) {
|
||||||
await this.apiService.refreshIdentityToken();
|
// Store the promise so multiple calls to refresh the token are not made
|
||||||
|
if (this.inFlightApiCalls.refreshToken === null) {
|
||||||
|
this.inFlightApiCalls.refreshToken = this.apiService.refreshIdentityToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.inFlightApiCalls.refreshToken;
|
||||||
}
|
}
|
||||||
const response = await this.apiService.getSync();
|
|
||||||
|
// Store the promise so multiple calls to sync are not made
|
||||||
|
if (this.inFlightApiCalls.sync === null) {
|
||||||
|
this.inFlightApiCalls.sync = this.apiService.getSync();
|
||||||
|
} else {
|
||||||
|
this.logService.debug(
|
||||||
|
"Sync: Sync network call already in progress, returning existing promise",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.inFlightApiCalls.sync;
|
||||||
|
|
||||||
await this.syncProfile(response.profile);
|
await this.syncProfile(response.profile);
|
||||||
await this.syncFolders(response.folders, response.profile.id);
|
await this.syncFolders(response.folders, response.profile.id);
|
||||||
@@ -162,6 +187,9 @@ export class DefaultSyncService extends CoreSyncService {
|
|||||||
} else {
|
} else {
|
||||||
return this.syncCompleted(false, userId);
|
return this.syncCompleted(false, userId);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.inFlightApiCalls.refreshToken = null;
|
||||||
|
this.inFlightApiCalls.sync = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user