Extract code-owners fetcher into a separate class
Change-Id: I0083787e93b16a471844e13d629a27c758a26e43
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index 49cb757..9f29d6d 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -138,7 +138,7 @@
* Periodical cache invalidation can lead to inconsistency in UI, i.e.
* user can see the old reviewers list (reflects a state when a change was
* loaded) and code-owners status for the current reviewer list. To avoid
- * this incostistency, the cache doesn't invalidated.
+ * this inconsistency, the cache doesn't invalidate.
*/
export class CodeOwnersCacheApi {
constructor(codeOwnerApi, change) {
@@ -181,6 +181,107 @@
}
}
+export class OwnersFetcher {
+ constructor(restApi, change, options) {
+ // fetched files and fetching status
+ this._fetchedOwners = new Map();
+ this._fetchStatus = FetchStatus.NOT_STARTED;
+ this._totalFetchCount = 0;
+ this.change = change;
+ this.options = options;
+ this.codeOwnerApi = new CodeOwnerApi(restApi);
+ }
+
+ getStatus() {
+ return this._fetchStatus;
+ }
+
+ getProgressString() {
+ return this._totalFetchCount === 0 ?
+ `Loading suggested owners ...` :
+ `${this._fetchedOwners.size} out of ${this._totalFetchCount} files have returned suggested owners.`;
+ }
+
+ getFiles() {
+ const result = [];
+ for (const [path, info] of this._fetchedOwners.entries()) {
+ result.push({path, info});
+ }
+ return result;
+ }
+
+ async fetchSuggestedOwners(codeOwnerStatusMap) {
+ // reset existing temporary storage
+ this._fetchedOwners = new Map();
+ this._fetchStatus = FetchStatus.FETCHING;
+ this._totalFetchCount = 0;
+
+ // only fetch those not approved yet
+ const filesGroupByStatus = [...codeOwnerStatusMap.keys()].reduce(
+ (list, file) => {
+ const status = codeOwnerStatusMap
+ .get(file).status;
+ if (status === OwnerStatus.INSUFFICIENT_REVIEWERS) {
+ list.missing.push(file);
+ } else if (status === OwnerStatus.PENDING) {
+ list.pending.push(file);
+ }
+ return list;
+ }
+ , {pending: [], missing: []});
+ // always fetch INSUFFICIENT_REVIEWERS first and then pending
+ const filesToFetch = filesGroupByStatus.missing
+ .concat(filesGroupByStatus.pending);
+ this._totalFetchCount = filesToFetch.length;
+ await this._batchFetchCodeOwners(filesToFetch);
+ this._fetchStatus = FetchStatus.FINISHED;
+ }
+
+ /**
+ * Recursively fetches code owners for all files until finished.
+ *
+ * @param {!Array<string>} files
+ */
+ async _batchFetchCodeOwners(files) {
+ if (this._fetchStatus === FetchStatus.ABORT) {
+ return this._fetchedOwners;
+ }
+
+ const batchRequests = [];
+ const maxConcurrentRequests = this.options.maxConcurrentRequests;
+ for (let i = 0; i < maxConcurrentRequests; i++) {
+ const filePath = files[i];
+ if (filePath) {
+ this._fetchedOwners.set(filePath, {});
+ batchRequests.push(this._fetchOwnersForPath(this.change.id, filePath));
+ }
+ }
+ const resPromise = Promise.all(batchRequests);
+ await resPromise;
+ if (files.length > maxConcurrentRequests) {
+ return await this._batchFetchCodeOwners(
+ files.slice(maxConcurrentRequests));
+ }
+ return this._fetchedOwners;
+ }
+
+ async _fetchOwnersForPath(changeId, filePath) {
+ try {
+ const owners = await this.codeOwnerApi.listOwnersForPath(changeId,
+ filePath);
+ this._fetchedOwners.get(filePath).owners = new Set(owners);
+ } catch (e) {
+ this._fetchedOwners.get(filePath).error = e;
+ }
+ }
+
+ abort() {
+ this._fetchStatus = FetchStatus.ABORT;
+ this._fetchedOwners = new Map();
+ this._totalFetchCount = 0;
+ }
+}
+
/**
* Service for the data layer used in the plugin UI.
*/
@@ -188,14 +289,13 @@
constructor(restApi, change, options = {}) {
this.restApi = restApi;
this.change = change;
- this.options = {maxConcurrentRequests: 10, ...options};
const codeOwnerApi = new CodeOwnerApi(restApi);
this.codeOwnerCacheApi = new CodeOwnersCacheApi(codeOwnerApi, change);
- // fetched files and fetching status
- this._fetchedOwners = new Map();
- this._fetchStatus = FetchStatus.NOT_STARTED;
- this._totalFetchCount = 0;
+ const fetcherOptions = {
+ maxConcurrentRequests: options.maxConcurrentRequests || 10,
+ };
+ this.ownersFetcher = new OwnersFetcher(restApi, change, fetcherOptions);
}
/**
@@ -319,56 +419,27 @@
* }}
*/
async getSuggestedOwners() {
+ const {codeOwnerStatusMap} = await this.getStatus();
+
// In case its aborted due to outdated patches
// should kick start the fetching again
// Note: we currently are not reusing the instance when switching changes,
// so if its `abort` due to different changes, the whole instance will be
// outdated and not used.
- if (this._fetchStatus === FetchStatus.NOT_STARTED
- || this._fetchStatus === FetchStatus.ABORT) {
- await this._fetchSuggestedOwners();
- this._fetchStatus = FetchStatus.FINISHED;
+ if (this.ownersFetcher.getStatus() === FetchStatus.NOT_STARTED
+ || this.ownersFetcher.getStatus() === FetchStatus.ABORT) {
+ await this.ownersFetcher.fetchSuggestedOwners(codeOwnerStatusMap);
}
- const {codeOwnerStatusMap} = await this.getStatus();
return {
- finished: this._fetchStatus === FetchStatus.FINISHED,
- status: this._fetchStatus,
- progress: this._totalFetchCount === 0 ?
- `Loading suggested owners ...` :
- `${this._fetchedOwners.size} out of ${this._totalFetchCount} ` +
- `files have returned suggested owners.`,
- suggestions: this._groupFilesByOwners(codeOwnerStatusMap),
+ finished: this.ownersFetcher.getStatus() === FetchStatus.FINISHED,
+ status: this.ownersFetcher.getStatus(),
+ progress: this.ownersFetcher.getProgressString(),
+ suggestions: this._groupFilesByOwners(codeOwnerStatusMap,
+ this.ownersFetcher.getFiles()),
};
}
- async _fetchSuggestedOwners() {
- // reset existing temporary storage
- this._fetchedOwners = new Map();
- this._fetchStatus = FetchStatus.FETCHING;
- this._totalFetchCount = 0;
-
- const {codeOwnerStatusMap} = await this.getStatus();
- // only fetch those not approved yet
- const filesGroupByStatus = [...codeOwnerStatusMap.keys()].reduce(
- (list, file) => {
- const status = codeOwnerStatusMap
- .get(file).status;
- if (status === OwnerStatus.INSUFFICIENT_REVIEWERS) {
- list.missing.push(file);
- } else if (status === OwnerStatus.PENDING) {
- list.pending.push(file);
- }
- return list;
- }
- , {pending: [], missing: []});
- // always fetch INSUFFICIENT_REVIEWERS first and then pending
- const filesToFetch = filesGroupByStatus.missing
- .concat(filesGroupByStatus.pending);
- this._totalFetchCount = filesToFetch.length;
- return await this._batchFetchCodeOwners(filesToFetch);
- }
-
_formatStatuses(statuses) {
// convert the array of statuses to map between file path -> status
return statuses.reduce((prev, cur) => {
@@ -402,28 +473,27 @@
return;
}
- _groupFilesByOwners(codeOwnerStatusMap) {
+ _groupFilesByOwners(codeOwnerStatusMap, files) {
// Note: for renamed or moved files, they will have two entries in the map
// we will treat them as two entries when group as well
- const allFiles = [...this._fetchedOwners.keys()];
const ownersFilesMap = new Map();
const failedToFetchFiles = new Set();
- for (let i = 0; i < allFiles.length; i++) {
+ for (const file of files) {
const fileInfo = {
- path: allFiles[i],
- status: this._computeFileStatus(codeOwnerStatusMap, allFiles[i]),
+ path: file.path,
+ status: this._computeFileStatus(codeOwnerStatusMap, file.path),
};
// for files failed to fetch, add them to the special group
- if (this._fetchedOwners.get(fileInfo.path).error) {
+ if (file.info.error) {
failedToFetchFiles.add(fileInfo);
continue;
}
// do not include files still in fetching
- if (!this._fetchedOwners.get(fileInfo.path).owners) {
+ if (!file.info.owners) {
continue;
}
- const owners = [...this._fetchedOwners.get(fileInfo.path).owners];
+ const owners = [...file.info.owners];
const ownersKey = owners
.map(owner => owner.account._account_id)
.sort()
@@ -470,47 +540,8 @@
return `${latestRevision._number}` === `${patchsetId}`;
}
- /**
- * Recursively fetches code owners for all files until finished.
- *
- * @param {!Array<string>} files
- */
- async _batchFetchCodeOwners(files) {
- if (this._fetchStatus === FetchStatus.ABORT) {
- return this._fetchedOwners;
- }
-
- const batchRequests = [];
- const maxConcurrentRequests = this.options.maxConcurrentRequests;
- for (let i = 0; i < maxConcurrentRequests; i++) {
- const filePath = files[i];
- if (filePath) {
- this._fetchedOwners.set(filePath, {});
- batchRequests.push(this._fetchOwnersForPath(this.change.id, filePath));
- }
- }
- const resPromise = Promise.all(batchRequests);
- await resPromise;
- if (files.length > maxConcurrentRequests) {
- return await this._batchFetchCodeOwners(
- files.slice(maxConcurrentRequests));
- }
- return this._fetchedOwners;
- }
-
- async _fetchOwnersForPath(changeId, filePath) {
- try {
- const owners = await this.codeOwnerCacheApi.listOwnersForPath(filePath);
- this._fetchedOwners.get(filePath).owners = new Set(owners);
- } catch (e) {
- this._fetchedOwners.get(filePath).error = e;
- }
- }
-
abort() {
- this._fetchStatus = FetchStatus.ABORT;
- this._fetchedOwners = new Map();
- this._totalFetchCount = 0;
+ this.ownersFetcher.abort();
const codeOwnerApi = new CodeOwnerApi(this.restApi);
this.codeOwnerCacheApi = new CodeOwnersCacheApi(codeOwnerApi, change);
}