diff --git a/src/system/decorators/serialize.ts b/src/system/decorators/serialize.ts index ccd5f57..43e2f81 100644 --- a/src/system/decorators/serialize.ts +++ b/src/system/decorators/serialize.ts @@ -1,9 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import { resolveProp } from './resolver'; - -export function serialize any>( - resolver?: (...args: Parameters) => 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)); - 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 | 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; }; }; diff --git a/src/webviews/commitDetails/commitDetailsWebviewView.ts b/src/webviews/commitDetails/commitDetailsWebviewView.ts index c066c53..86d8567 100644 --- a/src/webviews/commitDetails/commitDetailsWebviewView.ts +++ b/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 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._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>((c, name) => `${name}(${c.id})`) export abstract class WebviewBase 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 this.container.usage.track(`${this.trackingFeature}:shown`); @@ -126,6 +131,7 @@ export abstract class WebviewBase implements Disposable { protected includeBody?(): string | Promise; protected includeEndOfBody?(): string | Promise; + @debug() protected async refresh(force?: boolean): Promise { if (this._panel == null) return; @@ -159,11 +165,12 @@ export abstract class WebviewBase implements Disposable { this.onFocusChanged?.(e.webviewPanel.active); } + @debug['onMessageReceivedCore']>({ + args: { 0: e => (e != null ? `${e.id}: method=${e.method}` : '') }, + }) 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['postMessage']>({ args: { 0: m => `(id=${m.id}, method=${m.method})` } }) + private postMessage(message: IpcMessage): Promise { 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([ + this._panel.webview.postMessage(message), + new Promise(resolve => setTimeout(() => resolve(false), 5000)), + ]); } } diff --git a/src/webviews/webviewViewBase.ts b/src/webviews/webviewViewBase.ts index e2b8e6c..62bc47e 100644 --- a/src/webviews/webviewViewBase.ts +++ b/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>((c, name) => `${name}(${c.id})`) export abstract class WebviewViewBase implements WebviewViewProvider, Disposable { protected readonly disposables: Disposable[] = []; protected isReady: boolean = false; @@ -100,6 +103,7 @@ export abstract class WebviewViewBase implements protected includeBody?(): string | Promise; protected includeEndOfBody?(): string | Promise; + @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 { if (this._view == null) return; @@ -154,11 +159,12 @@ export abstract class WebviewViewBase implements this.onWindowFocusChanged?.(e.focused); } + @debug['onMessageReceivedCore']>({ + args: { 0: e => (e != null ? `${e.id}: method=${e.method}` : '') }, + }) 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['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([ + this._view.webview.postMessage(message), + new Promise(resolve => setTimeout(() => resolve(false), 5000)), + ]); } }