Introduce OwnersApiCache When Gerrit UI loads files owners status it treats each file as an independent UI component therefore as a result plugin's REST API endpoint is called for each one of them. The `Owned Files` tab adds an extra call on the top of that. The same applies for getting the currently logged in user account details. Reduce it to a single call per REST API endpoint by OwnersApiCache introduction (the idea is again transplanted from `code-owners` plugin). It is a safe bet as in the event of change modification (for instance when one votes) Gerrit doesn't modify the existing change object. It creates new change object instead and existing `OwnersService.getOwnersService` function (that is used to obtain the service) ensures that new service instance gets created (together with new instance of `OwnersApiCache`). Caching abilities are also verified with unit tests. Bug: Issue 377723880 Change-Id: I4b7312b644a0d2a608e4f19e23eda4d5a87f0b51
diff --git a/owners/web/owners-service.ts b/owners/web/owners-service.ts index 7a295da..2e80458 100644 --- a/owners/web/owners-service.ts +++ b/owners/web/owners-service.ts
@@ -118,16 +118,36 @@ } } -let service: OwnersService | undefined; +/** + * Calls to REST API can be safely cached cuz: + * 1. Gerrit doesn't update the existing change object but obtains and passes new instead + * 2. `OwnersService.getOwnersService` guarantees that whenever new change object is retrieved the new service instance is created (and cache is created with it) + */ +class OwnersApiCache { + private loggedInUser?: Promise<User>; -export class OwnersService { - private api: OwnersApi; + private filesOwners?: Promise<FilesOwners | undefined>; - constructor(readonly restApi: RestPluginApi, readonly change: ChangeInfo) { - this.api = new OwnersApi(restApi); + constructor( + private readonly api: OwnersApi, + private readonly change: ChangeInfo + ) {} + + getLoggedInUser(): Promise<User> { + if (this.loggedInUser === undefined) { + this.loggedInUser = this.getLoggedInUserImpl(); + } + return this.loggedInUser; } - async getLoggedInUser(): Promise<User> { + getFilesOwners(): Promise<FilesOwners | undefined> { + if (this.filesOwners === undefined) { + this.filesOwners = this.getFilesOwnersImpl(); + } + return this.filesOwners; + } + + private async getLoggedInUserImpl(): Promise<User> { const account = await this.api.getAccount(); if (!account) { return {role: UserRole.ANONYMOUS} as unknown as User; @@ -139,6 +159,28 @@ return {account, role} as unknown as User; } + private async getFilesOwnersImpl(): Promise<FilesOwners | undefined> { + return this.api.getFilesOwners( + this.change.project, + this.change._number, + this.change.current_revision ?? 'current' + ); + } +} + +let service: OwnersService | undefined; + +export class OwnersService { + private apiCache: OwnersApiCache; + + constructor(readonly restApi: RestPluginApi, readonly change: ChangeInfo) { + this.apiCache = new OwnersApiCache(new OwnersApi(restApi), change); + } + + getLoggedInUser(): Promise<User> { + return this.apiCache.getLoggedInUser(); + } + async getAllFilesApproved(): Promise<boolean | undefined> { if (!(await this.isLoggedIn())) { return Promise.resolve(undefined); @@ -163,12 +205,8 @@ ); } - async getFilesOwners(): Promise<FilesOwners | undefined> { - return this.api.getFilesOwners( - this.change.project, - this.change._number, - this.change.current_revision ?? 'current' - ); + getFilesOwners(): Promise<FilesOwners | undefined> { + return this.apiCache.getFilesOwners(); } private async isLoggedIn(): Promise<boolean> {
diff --git a/owners/web/owners-service_test.ts b/owners/web/owners-service_test.ts index 1c01c07..f98fddd 100644 --- a/owners/web/owners-service_test.ts +++ b/owners/web/owners-service_test.ts
@@ -106,6 +106,25 @@ assert.equal(user.role, UserRole.CHANGE_OWNER); assert.equal(user.account, owner); }); + + test('getLoggedInUser - should fetch response from plugin only once', async () => { + let calls = 0; + const notLoggedInApi = { + getLoggedIn() { + calls++; + return Promise.resolve(false); + }, + } as unknown as RestPluginApi; + + const service = OwnersService.getOwnersService( + notLoggedInApi, + fakeChange + ); + + await service.getLoggedInUser(); + await service.getLoggedInUser(); + assert.equal(calls, 1); + }); }); suite('files owners tests', () => { @@ -274,6 +293,18 @@ true ); }); + + test('should fetch response from plugin only once', async () => { + setupGetAllFilesApproved_true(); + + await service.getFilesOwners(); + await flush(); + + await service.getFilesOwners(); + await flush(); + + assert.equal(getApiStub.callCount, 1); + }); }); });