diff --git a/src/system/decorators.ts b/src/system/decorators.ts index 35b7bf6..bfa508a 100644 --- a/src/system/decorators.ts +++ b/src/system/decorators.ts @@ -1,5 +1,6 @@ 'use strict'; import { Logger, LogLevel } from '../logger'; +import { Functions } from './function'; import { Strings } from './string'; function decorate(decorator: (fn: Function, key: string) => Function): Function { @@ -177,11 +178,7 @@ export function log( const start = options.timed ? process.hrtime() : undefined; const result = fn.apply(this, args); - if ( - result != null && - (typeof result === 'object' || typeof result === 'function') && - typeof result.then === 'function' - ) { + if (result != null && Functions.isPromise(result)) { const promise = result.then((r: any) => { const timing = start !== undefined ? ` \u2022 ${Strings.getDurationMilliseconds(start)} ms` : ''; @@ -224,6 +221,41 @@ export function log( }; } +export function gate() { + return (target: any, key: string, descriptor: PropertyDescriptor) => { + if (!(typeof descriptor.value === 'function')) throw new Error('not supported'); + + const gateKey = `$gate$${key}`; + const fn = descriptor.value; + + descriptor.value = function(this: any, ...args: any[]) { + if (!this.hasOwnProperty(gateKey)) { + Object.defineProperty(this, gateKey, { + configurable: false, + enumerable: false, + writable: true, + value: undefined + }); + } + + let promise = this[gateKey]; + if (promise === undefined) { + const result = fn.apply(this, args); + if (result == null || !Functions.isPromise(result)) { + return result; + } + + this[gateKey] = promise = result.then((r: any) => { + this[gateKey] = undefined; + return r; + }); + } + + return promise; + }; + }; +} + function _memoize(fn: Function, key: string): Function { const memoizeKey = `$memoize$${key}`; diff --git a/src/system/function.ts b/src/system/function.ts index 7693051..6392c19 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -57,6 +57,10 @@ export namespace Functions { return tracked; } + export function isPromise(o: any) { + return (typeof o === 'object' || typeof o === 'function') && typeof o.then === 'function'; + } + export function once(fn: T): T { return _once(fn); } diff --git a/src/views/nodes/fileHistoryTrackerNode.ts b/src/views/nodes/fileHistoryTrackerNode.ts index b145d81..c52dd29 100644 --- a/src/views/nodes/fileHistoryTrackerNode.ts +++ b/src/views/nodes/fileHistoryTrackerNode.ts @@ -4,7 +4,7 @@ import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, Uri, window import { UriComparer } from '../../comparers'; import { Container } from '../../container'; import { GitUri } from '../../git/gitService'; -import { debug, Functions, log } from '../../system'; +import { debug, Functions, gate, log } from '../../system'; import { FileHistoryView } from '../fileHistoryView'; import { MessageNode } from './common'; import { FileHistoryNode } from './fileHistoryNode'; @@ -52,6 +52,8 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode { return item; } + @gate() @log() async fetchAll() { if (this._children === undefined || this._children.length === 0) return; @@ -86,6 +87,7 @@ export class RepositoriesNode extends SubscribeableViewNode { ); } + @gate() @log() async pullAll() { if (this._children === undefined || this._children.length === 0) return; @@ -115,6 +117,8 @@ export class RepositoriesNode extends SubscribeableViewNode { ); } + @gate() + @debug() async refresh(reason?: RefreshReason) { if (this._children === undefined) return; diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index fa1324f..2642f06 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -13,7 +13,7 @@ import { RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../../git/gitService'; -import { Dates, debug, Functions, log, Strings } from '../../system'; +import { Dates, debug, Functions, gate, log, Strings } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { BranchesNode } from './branchesNode'; import { BranchNode } from './branchNode'; @@ -168,6 +168,7 @@ export class RepositoryNode extends SubscribeableViewNode { return item; } + @gate() @log() async fetch(progress: boolean = true) { if (!progress) return this.fetchCore(); @@ -189,6 +190,7 @@ export class RepositoryNode extends SubscribeableViewNode { this.view.triggerNodeChange(this); } + @gate() @log() async pull(progress: boolean = true) { if (!progress) return this.pullCore(); @@ -210,6 +212,7 @@ export class RepositoryNode extends SubscribeableViewNode { this.view.triggerNodeChange(this); } + @gate() @log() async push(progress: boolean = true) { if (!progress) return this.pushCore(); @@ -230,11 +233,13 @@ export class RepositoryNode extends SubscribeableViewNode { this.view.triggerNodeChange(this); } - refresh() { + @gate() + @debug() + async refresh() { this._status = this.repo.getStatus(); this._children = undefined; - void this.ensureSubscription(); + await this.ensureSubscription(); } @debug() diff --git a/src/views/nodes/resultsCommitsNode.ts b/src/views/nodes/resultsCommitsNode.ts index cda49bd..db95a6c 100644 --- a/src/views/nodes/resultsCommitsNode.ts +++ b/src/views/nodes/resultsCommitsNode.ts @@ -53,7 +53,7 @@ export class ResultsCommitsNode extends ViewNode implements PageableViewNode { return item; } - async refresh() { + refresh() { this._commitsQueryResults = this._commitsQuery(this.maxCount); } diff --git a/src/views/nodes/resultsFilesNode.ts b/src/views/nodes/resultsFilesNode.ts index 7f10239..17b6ad8 100644 --- a/src/views/nodes/resultsFilesNode.ts +++ b/src/views/nodes/resultsFilesNode.ts @@ -63,7 +63,7 @@ export class ResultsFilesNode extends ViewNode { return item; } - async refresh() { + refresh() { this._filesQueryResults = this.getFilesQueryResultsCore(); } diff --git a/src/views/nodes/resultsNode.ts b/src/views/nodes/resultsNode.ts index 72849b7..70918e6 100644 --- a/src/views/nodes/resultsNode.ts +++ b/src/views/nodes/resultsNode.ts @@ -3,7 +3,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ShowCommitSearchCommandArgs } from '../../commands'; import { GlyphChars } from '../../constants'; import { GitRepoSearchBy } from '../../git/gitService'; -import { Strings } from '../../system'; +import { debug, Functions, gate, log, Strings } from '../../system'; import { ResultsView } from '../resultsView'; import { CommandMessageNode, MessageNode } from './common'; import { ResourceType, unknownGitUri, ViewNode } from './viewNode'; @@ -114,6 +114,7 @@ export class ResultsNode extends ViewNode { this.view.triggerNodeChange(); } + @log() clear() { if (this._children.length === 0) return; @@ -121,6 +122,9 @@ export class ResultsNode extends ViewNode { this.view.triggerNodeChange(); } + @log({ + args: { 0: (n: ViewNode) => n.toString() } + }) dismiss(node: ViewNode) { if (this._children.length === 0) return; @@ -131,9 +135,11 @@ export class ResultsNode extends ViewNode { this.view.triggerNodeChange(); } + @gate() + @debug() async refresh() { if (this._children.length === 0) return; - this._children.forEach(c => c.refresh()); + await Promise.all(this._children.map(c => c.refresh()).filter(Functions.isPromise) as Promise[]); } } diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index 726f0c6..630d848 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { Command, Disposable, Event, TreeItem, TreeItemCollapsibleState, TreeViewVisibilityChangeEvent } from 'vscode'; import { GitUri } from '../../git/gitService'; -import { debug, logName } from '../../system'; +import { debug, gate, logName } from '../../system'; import { RefreshReason, TreeViewNodeStateChangeEvent, View } from '../viewBase'; export enum ResourceType { @@ -65,6 +65,10 @@ export abstract class ViewNode { this._uri = uri; } + toString() { + return `${this.constructor.name}${this.id != null ? `(${this.id})` : ''}`; + } + protected _uri: GitUri; get uri() { return this._uri; @@ -82,6 +86,7 @@ export abstract class ViewNode { return undefined; } + @gate() @debug() refresh(reason?: RefreshReason): void | boolean | Promise | Promise {} } diff --git a/src/views/viewBase.ts b/src/views/viewBase.ts index 5b45c01..4fd22f2 100644 --- a/src/views/viewBase.ts +++ b/src/views/viewBase.ts @@ -148,7 +148,7 @@ export abstract class ViewBase implements TreeDataProvid } @debug({ - args: { 0: (n: ViewNode) => `${n.constructor.name}${n.id != null ? `(${n.id})` : ''}` } + args: { 0: (n: ViewNode) => n.toString() } }) async refreshNode(node: ViewNode, args?: RefreshNodeCommandArgs) { if (args !== undefined) { @@ -169,7 +169,7 @@ export abstract class ViewBase implements TreeDataProvid } @log({ - args: { 0: (n: ViewNode) => `${n.constructor.name}${n.id != null ? `(${n.id})` : ''}` } + args: { 0: (n: ViewNode) => n.toString() } }) async reveal( node: ViewNode, @@ -198,7 +198,7 @@ export abstract class ViewBase implements TreeDataProvid } @debug({ - args: { 0: (n?: ViewNode) => (n != null ? `${n.constructor.name}${n.id != null ? `(${n.id})` : ''}` : '') } + args: { 0: (n: ViewNode) => (n != null ? n.toString() : '') } }) triggerNodeChange(node?: ViewNode) { // Since the root node won't actually refresh, force everything