소스 검색

Avoids inifinite spinner on Commit Details

It seems like there is a vscode/webview bug that sometimes causes `postMessage` to hang indefinitely
main
Eric Amodio 2 년 전
부모
커밋
615b51a9b2
4개의 변경된 파일41개의 추가작업 그리고 36개의 파일을 삭제
  1. +8
    -13
      src/system/decorators/serialize.ts
  2. +0
    -14
      src/webviews/commitDetails/commitDetailsWebviewView.ts
  3. +17
    -5
      src/webviews/webviewBase.ts
  4. +16
    -4
      src/webviews/webviewViewBase.ts

+ 8
- 13
src/system/decorators/serialize.ts 파일 보기

@ -1,9 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { resolveProp } from './resolver';
export function serialize<T extends (...arg: any) => any>(
resolver?: (...args: Parameters<T>) => string,
): (target: any, key: string, descriptor: PropertyDescriptor) => void {
export function serialize(): (target: any, key: string, descriptor: PropertyDescriptor) => void {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
let fn: Function | undefined;
if (typeof descriptor.value === 'function') {
@ -16,9 +11,8 @@ export function serialize any>(
const serializeKey = `$serialize$${key}`;
descriptor.value = function (this: any, ...args: any[]) {
const prop = resolveProp(serializeKey, resolver, ...(args as Parameters<T>));
if (!Object.prototype.hasOwnProperty.call(this, prop)) {
Object.defineProperty(this, prop, {
if (!Object.prototype.hasOwnProperty.call(this, serializeKey)) {
Object.defineProperty(this, serializeKey, {
configurable: false,
enumerable: false,
writable: true,
@ -26,15 +20,16 @@ export function serialize any>(
});
}
let promise = this[prop];
const run = () => fn!.apply(this, args);
if (promise === undefined) {
let promise: Promise<any> | undefined = this[serializeKey];
// eslint-disable-next-line no-return-await, @typescript-eslint/no-unsafe-return
const run = async () => await fn!.apply(this, args);
if (promise == null) {
promise = run();
} else {
promise = promise.then(run, run);
}
this[prop] = promise;
this[serializeKey] = promise;
return promise;
};
};

+ 0
- 14
src/webviews/commitDetails/commitDetailsWebviewView.ts 파일 보기

@ -22,7 +22,6 @@ import { debounce } from '../../system/function';
import { getSettledValue } from '../../system/promise';
import type { Serialized } from '../../system/serialize';
import { serialize } from '../../system/serialize';
import { Stopwatch } from '../../system/stopwatch';
import type { LinesChangeEvent } from '../../trackers/lineTracker';
import { CommitFileNode } from '../../views/nodes/commitFileNode';
import { CommitNode } from '../../views/nodes/commitNode';
@ -475,7 +474,6 @@ export class CommitDetailsWebviewView extends WebviewViewBase
private _notifyDidChangeStateDebounced: Deferrable<() => void> | undefined = undefined;
@debug()
private updateState(immediate: boolean = false) {
if (!this.isReady || !this.visible) return;
@ -491,9 +489,6 @@ export class CommitDetailsWebviewView extends WebviewViewBase
this._notifyDidChangeStateDebounced();
}
private _counter = 0;
@debug()
private async notifyDidChangeState() {
if (!this.isReady || !this.visible) return false;
@ -505,11 +500,6 @@ export class CommitDetailsWebviewView extends WebviewViewBase
const context = { ...this._context, ...this._pendingContext };
return window.withProgress({ location: { viewId: this.id } }, async () => {
this._counter++;
const sw = new Stopwatch(scope);
Logger.warn(scope, `(${this._counter}) starting...`);
try {
const success = await this.notify(DidChangeStateNotificationType, {
state: await this.getState(context),
@ -522,10 +512,6 @@ export class CommitDetailsWebviewView extends WebviewViewBase
Logger.error(scope, ex);
debugger;
}
sw.stop();
Logger.warn(scope, `(${this._counter}) completed after ${sw.elapsed()} ms`);
});
}

+ 17
- 5
src/webviews/webviewBase.ts 파일 보기

@ -5,6 +5,8 @@ import type { Commands } from '../constants';
import type { Container } from '../container';
import { Logger } from '../logger';
import { executeCommand, registerCommand } from '../system/command';
import { debug, log, logName } from '../system/decorators/log';
import { serialize } from '../system/decorators/serialize';
import type { TrackedUsageFeatures } from '../usageTracker';
import type { IpcMessage, IpcMessageParams, IpcNotificationType } from './protocol';
import { ExecuteCommandType, onIpc, WebviewReadyCommandType } from './protocol';
@ -22,6 +24,7 @@ function nextIpcId() {
return `host:${ipcSequence}`;
}
@logName<WebviewBase<any>>((c, name) => `${name}(${c.id})`)
export abstract class WebviewBase<State> implements Disposable {
protected readonly disposables: Disposable[] = [];
protected isReady: boolean = false;
@ -66,10 +69,12 @@ export abstract class WebviewBase implements Disposable {
return this._panel?.visible ?? false;
}
@log()
hide() {
this._panel?.dispose();
}
@log({ args: false })
async show(column: ViewColumn = ViewColumn.Beside, ..._args: unknown[]): Promise<void> {
void this.container.usage.track(`${this.trackingFeature}:shown`);
@ -126,6 +131,7 @@ export abstract class WebviewBase implements Disposable {
protected includeBody?(): string | Promise<string>;
protected includeEndOfBody?(): string | Promise<string>;
@debug()
protected async refresh(force?: boolean): Promise<void> {
if (this._panel == null) return;
@ -159,11 +165,12 @@ export abstract class WebviewBase implements Disposable {
this.onFocusChanged?.(e.webviewPanel.active);
}
@debug<WebviewBase<State>['onMessageReceivedCore']>({
args: { 0: e => (e != null ? `${e.id}: method=${e.method}` : '<undefined>') },
})
protected onMessageReceivedCore(e: IpcMessage) {
if (e == null) return;
Logger.debug(`Webview(${this.id}).onMessageReceived: method=${e.method}`);
switch (e.method) {
case WebviewReadyCommandType.method:
onIpc(WebviewReadyCommandType, e, () => {
@ -245,10 +252,15 @@ export abstract class WebviewBase implements Disposable {
return this.postMessage({ id: nextIpcId(), method: type.method, params: params });
}
private postMessage(message: IpcMessage) {
@serialize()
@debug<WebviewBase<State>['postMessage']>({ args: { 0: m => `(id=${m.id}, method=${m.method})` } })
private postMessage(message: IpcMessage): Promise<boolean> {
if (this._panel == null) return Promise.resolve(false);
Logger.debug(`Webview(${this.id}).postMessage: method=${message.method}`);
return this._panel.webview.postMessage(message);
// It looks like there is a bug where `postMessage` can sometimes just hang infinitely. Not sure why, but ensure we don't hang
return Promise.race<boolean>([
this._panel.webview.postMessage(message),
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 5000)),
]);
}
}

+ 16
- 4
src/webviews/webviewViewBase.ts 파일 보기

@ -12,10 +12,12 @@ import type { Commands } from '../constants';
import type { Container } from '../container';
import { Logger } from '../logger';
import { executeCommand } from '../system/command';
import { getLogScope, log } from '../system/decorators/log';
import { debug, getLogScope, log, logName } from '../system/decorators/log';
import { serialize } from '../system/decorators/serialize';
import type { TrackedUsageFeatures } from '../usageTracker';
import type { IpcMessage, IpcMessageParams, IpcNotificationType } from './protocol';
import { ExecuteCommandType, onIpc, WebviewReadyCommandType } from './protocol';
import type { WebviewBase } from './webviewBase';
const maxSmallIntegerV8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers)
@ -30,6 +32,7 @@ function nextIpcId() {
return `host:${ipcSequence}`;
}
@logName<WebviewViewBase<any>>((c, name) => `${name}(${c.id})`)
export abstract class WebviewViewBase<State, SerializedState = State> implements WebviewViewProvider, Disposable {
protected readonly disposables: Disposable[] = [];
protected isReady: boolean = false;
@ -100,6 +103,7 @@ export abstract class WebviewViewBase implements
protected includeBody?(): string | Promise<string>;
protected includeEndOfBody?(): string | Promise<string>;
@debug({ args: false })
async resolveWebviewView(
webviewView: WebviewView,
_context: WebviewViewResolveContext,
@ -127,6 +131,7 @@ export abstract class WebviewViewBase implements
this.onVisibilityChanged?.(true);
}
@debug()
protected async refresh(): Promise<void> {
if (this._view == null) return;
@ -154,11 +159,12 @@ export abstract class WebviewViewBase implements
this.onWindowFocusChanged?.(e.focused);
}
@debug<WebviewViewBase<State>['onMessageReceivedCore']>({
args: { 0: e => (e != null ? `${e.id}: method=${e.method}` : '<undefined>') },
})
private onMessageReceivedCore(e: IpcMessage) {
if (e == null) return;
Logger.debug(`WebviewView(${this.id}).onMessageReceived: method=${e.method}`);
switch (e.method) {
case WebviewReadyCommandType.method:
onIpc(WebviewReadyCommandType, e, () => {
@ -241,9 +247,15 @@ export abstract class WebviewViewBase implements
return this.postMessage({ id: nextIpcId(), method: type.method, params: params });
}
@serialize()
@debug<WebviewBase<State>['postMessage']>({ args: { 0: m => `(id=${m.id}, method=${m.method})` } })
protected postMessage(message: IpcMessage) {
if (this._view == null) return Promise.resolve(false);
return this._view.webview.postMessage(message);
// It looks like there is a bug where `postMessage` can sometimes just hang infinitely. Not sure why, but ensure we don't hang
return Promise.race<boolean>([
this._view.webview.postMessage(message),
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 5000)),
]);
}
}

불러오는 중...
취소
저장