| /** | 
 |  * @license | 
 |  * Copyright (C) 2021 The Android Open Source Project | 
 |  * | 
 |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
 |  * you may not use this file except in compliance with the License. | 
 |  * You may obtain a copy of the License at | 
 |  * | 
 |  * http://www.apache.org/licenses/LICENSE-2.0 | 
 |  * | 
 |  * Unless required by applicable law or agreed to in writing, software | 
 |  * distributed under the License is distributed on an "AS IS" BASIS, | 
 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 |  * See the License for the specific language governing permissions and | 
 |  * limitations under the License. | 
 |  */ | 
 |  | 
 | import {CodeOwnersApi} from './code-owners-api.js'; | 
 |  | 
 | /** | 
 |  * All statuses returned for owner status. | 
 |  * | 
 |  * @enum | 
 |  */ | 
 | export const OwnerStatus = { | 
 |   INSUFFICIENT_REVIEWERS: 'INSUFFICIENT_REVIEWERS', | 
 |   PENDING: 'PENDING', | 
 |   APPROVED: 'APPROVED', | 
 | }; | 
 |  | 
 | /** | 
 |  * @enum | 
 |  */ | 
 | export const FetchStatus = { | 
 |   /** Fetch hasn't been started */ | 
 |   NOT_STARTED: 0, | 
 |   /** | 
 |    * Fetch has been started, but not all files has been finished. | 
 |    * Pausing during fetching doesn't change state. | 
 |    */ | 
 |   FETCHING: 1, | 
 |   /** | 
 |    * All owners has been loaded. resume/pause call doesn't change state. | 
 |    */ | 
 |   FINISHED: 2, | 
 | }; | 
 |  | 
 | /** | 
 |  * Fetch owners for files. The class fetches owners in parallel and allows to | 
 |  * pause/resume fetch. | 
 |  */ | 
 | class OwnersFetcher { | 
 |   /** | 
 |    * Creates a fetcher in paused state. Actual fetching starts after resume() | 
 |    * is called. | 
 |    * | 
 |    * @param {Array<string>} filesToFetch - Files paths for loading owners. | 
 |    * @param {number} ownersLimit - number of requested owners per file. | 
 |    * @param {number} maxConcurrentRequest - max number of concurrent requests to server. | 
 |    */ | 
 |   constructor(codeOwnerApi, changeId, filesToFetch, ownersLimit, | 
 |       maxConcurrentRequest) { | 
 |     this._fetchedOwners = new Map(); | 
 |     this._ownersLimit = ownersLimit; | 
 |     this._paused = true; | 
 |     this._pausedFilesFetcher = []; | 
 |     this._filesToFetch = filesToFetch; | 
 |     this._fetchFilesPromises = []; | 
 |     this._codeOwnerApi = codeOwnerApi; | 
 |     this._changeId = changeId; | 
 |  | 
 |     for (let i = 0; i < maxConcurrentRequest; i++) { | 
 |       this._fetchFilesPromises.push(this._fetchFiles()); | 
 |     } | 
 |   } | 
 |  | 
 |   async _fetchFiles() { | 
 |     for (;;) { | 
 |       const filePath = await this._getNextFilePath(); | 
 |       if (!filePath) return; | 
 |       try { | 
 |         this._fetchedOwners.set(filePath, { | 
 |           owners: await this._codeOwnerApi.listOwnersForPath(this._changeId, | 
 |               filePath, this._ownersLimit), | 
 |         }); | 
 |       } catch (error) { | 
 |         this._fetchedOwners.set(filePath, {error}); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   async _getNextFilePath() { | 
 |     if (this._paused) { | 
 |       await new Promise(resolve => this._pausedFilesFetcher.push(resolve)); | 
 |     } | 
 |     if (this._filesToFetch.length === 0) return null; | 
 |     return this._filesToFetch.splice(0, 1)[0]; | 
 |   } | 
 |  | 
 |   async waitFetchComplete() { | 
 |     await Promise.allSettled(this._fetchFilesPromises); | 
 |   } | 
 |  | 
 |   resume() { | 
 |     if (!this._paused) return; | 
 |     this._paused = false; | 
 |     for (const fetcher of this._pausedFilesFetcher.splice(0, | 
 |         this._pausedFilesFetcher.length)) { | 
 |       fetcher(); | 
 |     } | 
 |   } | 
 |  | 
 |   pause() { | 
 |     this._paused = true; | 
 |   } | 
 |  | 
 |   getFetchedOwners() { | 
 |     return this._fetchedOwners; | 
 |   } | 
 |  | 
 |   getFiles() { | 
 |     const result = []; | 
 |     for (const [path, info] of this._fetchedOwners.entries()) { | 
 |       result.push({path, info}); | 
 |     } | 
 |     return result; | 
 |   } | 
 | } | 
 |  | 
 | export class OwnersProvider { | 
 |   constructor(restApi, change, options) { | 
 |     this.change = change; | 
 |     this.options = options; | 
 |     this._totalFetchCount = 0; | 
 |     this._status = FetchStatus.NOT_STARTED; | 
 |     this._codeOwnerApi = new CodeOwnersApi(restApi); | 
 |   } | 
 |  | 
 |   getStatus() { | 
 |     return this._status; | 
 |   } | 
 |  | 
 |   getProgressString() { | 
 |     return !this._ownersFetcher || this._totalFetchCount === 0 ? | 
 |       `Loading suggested owners ...` : | 
 |       `${this._ownersFetcher.getFetchedOwners().size} out of ` + | 
 |       `${this._totalFetchCount} files have returned suggested owners.`; | 
 |   } | 
 |  | 
 |   getFiles() { | 
 |     if (!this._ownersFetcher) return []; | 
 |     return this._ownersFetcher.getFiles(); | 
 |   } | 
 |  | 
 |   async fetchSuggestedOwners(codeOwnerStatusMap) { | 
 |     if (this._status !== FetchStatus.NOT_STARTED) { | 
 |       await this._ownersFetcher.waitFetchComplete(); | 
 |       return; | 
 |     } | 
 |     const filesToFetch = this._getFilesToFetch(codeOwnerStatusMap); | 
 |     this._totalFetchCount = filesToFetch.length; | 
 |     this._ownersFetcher = new OwnersFetcher(this._codeOwnerApi, this.change.id, | 
 |         filesToFetch, | 
 |         this.options.ownersLimit, this.options.maxConcurrentRequests); | 
 |     this._status = FetchStatus.FETCHING; | 
 |     this._ownersFetcher.resume(); | 
 |     await this._ownersFetcher.waitFetchComplete(filesToFetch); | 
 |     this._status = FetchStatus.FINISHED; | 
 |   } | 
 |  | 
 |   _getFilesToFetch(codeOwnerStatusMap) { | 
 |     // only fetch those not approved yet | 
 |     const filesGroupByStatus = [...codeOwnerStatusMap.entries()].reduce( | 
 |         (list, [file, fileInfo]) => { | 
 |           if (list[fileInfo.status]) list[fileInfo.status].push(file); | 
 |           return list; | 
 |         } | 
 |         , { | 
 |           [OwnerStatus.PENDING]: [], | 
 |           [OwnerStatus.INSUFFICIENT_REVIEWERS]: [], | 
 |           [OwnerStatus.APPROVED]: [], | 
 |         } | 
 |     ); | 
 |     // always fetch INSUFFICIENT_REVIEWERS first, then pending and then approved | 
 |     return filesGroupByStatus[OwnerStatus.INSUFFICIENT_REVIEWERS] | 
 |         .concat(filesGroupByStatus[OwnerStatus.PENDING]) | 
 |         .concat(filesGroupByStatus[OwnerStatus.APPROVED]); | 
 |   } | 
 |  | 
 |   pause() { | 
 |     if (!this._ownersFetcher) return; | 
 |     this._ownersFetcher.pause(); | 
 |   } | 
 |  | 
 |   resume() { | 
 |     if (!this._ownersFetcher) return; | 
 |     this._ownersFetcher.resume(); | 
 |   } | 
 |  | 
 |   reset() { | 
 |     this._totalFetchCount = 0; | 
 |     this.ownersFetcher = null; | 
 |     this._status = FetchStatus.NOT_STARTED; | 
 |   } | 
 | } |