/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AutorunObserver } from './autorun.js'; import { IObservable, TransactionImpl } from './base.js'; import { Derived } from './derived.js'; import { FromEventObservable } from './utils.js'; let globalObservableLogger: IObservableLogger | undefined; export function setLogger(logger: IObservableLogger): void { globalObservableLogger = logger; } export function getLogger(): IObservableLogger | undefined { return globalObservableLogger; } export function logObservable(obs: IObservable): void { if (!globalObservableLogger) { const l = new ConsoleObservableLogger(); l.addFilteredObj(obs); setLogger(l); } else { if (globalObservableLogger instanceof ConsoleObservableLogger) { (globalObservableLogger as ConsoleObservableLogger).addFilteredObj(obs); } } } interface IChangeInformation { oldValue: unknown; newValue: unknown; change: unknown; didChange: boolean; hadValue: boolean; } export interface IObservableLogger { handleObservableChanged(observable: IObservable, info: IChangeInformation): void; handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void; handleAutorunCreated(autorun: AutorunObserver): void; handleAutorunTriggered(autorun: AutorunObserver): void; handleAutorunFinished(autorun: AutorunObserver): void; handleDerivedCreated(observable: Derived): void; handleDerivedRecomputed(observable: Derived, info: IChangeInformation): void; handleDerivedCleared(observable: Derived): void; handleBeginTransaction(transaction: TransactionImpl): void; handleEndTransaction(): void; } export class ConsoleObservableLogger implements IObservableLogger { private indentation = 0; private _filteredObjects: Set | undefined; public addFilteredObj(obj: unknown): void { if (!this._filteredObjects) { this._filteredObjects = new Set(); } this._filteredObjects.add(obj); } private _isIncluded(obj: unknown): boolean { return this._filteredObjects?.has(obj) ?? true; } private textToConsoleArgs(text: ConsoleText): unknown[] { return consoleTextToArgs([ normalText(repeat('| ', this.indentation)), text, ]); } private formatInfo(info: IChangeInformation): ConsoleText[] { if (!info.hadValue) { return [ normalText(` `), styled(formatValue(info.newValue, 60), { color: 'green', }), normalText(` (initial)`), ]; } return info.didChange ? [ normalText(` `), styled(formatValue(info.oldValue, 70), { color: 'red', strikeThrough: true, }), normalText(` `), styled(formatValue(info.newValue, 60), { color: 'green', }), ] : [normalText(` (unchanged)`)]; } handleObservableChanged(observable: IObservable, info: IChangeInformation): void { if (!this._isIncluded(observable)) { return; } console.log(...this.textToConsoleArgs([ formatKind('observable value changed'), styled(observable.debugName, { color: 'BlueViolet' }), ...this.formatInfo(info), ])); } private readonly changedObservablesSets = new WeakMap>>(); formatChanges(changes: Set>): ConsoleText | undefined { if (changes.size === 0) { return undefined; } return styled( ' (changed deps: ' + [...changes].map((o) => o.debugName).join(', ') + ')', { color: 'gray' } ); } handleDerivedCreated(derived: Derived): void { const existingHandleChange = derived.handleChange; this.changedObservablesSets.set(derived, new Set()); derived.handleChange = (observable, change) => { this.changedObservablesSets.get(derived)!.add(observable); return existingHandleChange.apply(derived, [observable, change]); }; const debugTrackUpdating = false; if (debugTrackUpdating) { const updating: IObservable[] = []; (derived as any).__debugUpdating = updating; const existingBeginUpdate = derived.beginUpdate; derived.beginUpdate = (obs) => { updating.push(obs); return existingBeginUpdate.apply(derived, [obs]); }; const existingEndUpdate = derived.endUpdate; derived.endUpdate = (obs) => { const idx = updating.indexOf(obs); if (idx === -1) { console.error('endUpdate called without beginUpdate', derived.debugName, obs.debugName); } updating.splice(idx, 1); return existingEndUpdate.apply(derived, [obs]); }; } } handleDerivedRecomputed(derived: Derived, info: IChangeInformation): void { if (!this._isIncluded(derived)) { return; } const changedObservables = this.changedObservablesSets.get(derived); if (!changedObservables) { return; } console.log(...this.textToConsoleArgs([ formatKind('derived recomputed'), styled(derived.debugName, { color: 'BlueViolet' }), ...this.formatInfo(info), this.formatChanges(changedObservables), { data: [{ fn: derived._debugNameData.referenceFn ?? derived._computeFn }] } ])); changedObservables.clear(); } handleDerivedCleared(derived: Derived): void { if (!this._isIncluded(derived)) { return; } console.log(...this.textToConsoleArgs([ formatKind('derived cleared'), styled(derived.debugName, { color: 'BlueViolet' }), ])); } handleFromEventObservableTriggered(observable: FromEventObservable, info: IChangeInformation): void { if (!this._isIncluded(observable)) { return; } console.log(...this.textToConsoleArgs([ formatKind('observable from event triggered'), styled(observable.debugName, { color: 'BlueViolet' }), ...this.formatInfo(info), { data: [{ fn: observable._getValue }] } ])); } handleAutorunCreated(autorun: AutorunObserver): void { if (!this._isIncluded(autorun)) { return; } const existingHandleChange = autorun.handleChange; this.changedObservablesSets.set(autorun, new Set()); autorun.handleChange = (observable, change) => { this.changedObservablesSets.get(autorun)!.add(observable); return existingHandleChange.apply(autorun, [observable, change]); }; } handleAutorunTriggered(autorun: AutorunObserver): void { const changedObservables = this.changedObservablesSets.get(autorun); if (!changedObservables) { return; } if (this._isIncluded(autorun)) { console.log(...this.textToConsoleArgs([ formatKind('autorun'), styled(autorun.debugName, { color: 'BlueViolet' }), this.formatChanges(changedObservables), { data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] } ])); } changedObservables.clear(); this.indentation++; } handleAutorunFinished(autorun: AutorunObserver): void { this.indentation--; } handleBeginTransaction(transaction: TransactionImpl): void { let transactionName = transaction.getDebugName(); if (transactionName === undefined) { transactionName = ''; } if (this._isIncluded(transaction)) { console.log(...this.textToConsoleArgs([ formatKind('transaction'), styled(transactionName, { color: 'BlueViolet' }), { data: [{ fn: transaction._fn }] } ])); } this.indentation++; } handleEndTransaction(): void { this.indentation--; } } type ConsoleText = | (ConsoleText | undefined)[] | { text: string; style: string; data?: unknown[] } | { data: unknown[] }; function consoleTextToArgs(text: ConsoleText): unknown[] { const styles = new Array(); const data: unknown[] = []; let firstArg = ''; function process(t: ConsoleText): void { if ('length' in t) { for (const item of t) { if (item) { process(item); } } } else if ('text' in t) { firstArg += `%c${t.text}`; styles.push(t.style); if (t.data) { data.push(...t.data); } } else if ('data' in t) { data.push(...t.data); } } process(text); const result = [firstArg, ...styles]; result.push(...data); return result; } function normalText(text: string): ConsoleText { return styled(text, { color: 'black' }); } function formatKind(kind: string): ConsoleText { return styled(padStr(`${kind}: `, 10), { color: 'black', bold: true }); } function styled( text: string, options: { color: string; strikeThrough?: boolean; bold?: boolean } = { color: 'black', } ): ConsoleText { function objToCss(styleObj: Record): string { return Object.entries(styleObj).reduce( (styleString, [propName, propValue]) => { return `${styleString}${propName}:${propValue};`; }, '' ); } const style: Record = { color: options.color, }; if (options.strikeThrough) { style['text-decoration'] = 'line-through'; } if (options.bold) { style['font-weight'] = 'bold'; } return { text, style: objToCss(style), }; } function formatValue(value: unknown, availableLen: number): string { switch (typeof value) { case 'number': return '' + value; case 'string': if (value.length + 2 <= availableLen) { return `"${value}"`; } return `"${value.substr(0, availableLen - 7)}"+...`; case 'boolean': return value ? 'true' : 'false'; case 'undefined': return 'undefined'; case 'object': if (value === null) { return 'null'; } if (Array.isArray(value)) { return formatArray(value, availableLen); } return formatObject(value, availableLen); case 'symbol': return value.toString(); case 'function': return `[[Function${value.name ? ' ' + value.name : ''}]]`; default: return '' + value; } } function formatArray(value: unknown[], availableLen: number): string { let result = '[ '; let first = true; for (const val of value) { if (!first) { result += ', '; } if (result.length - 5 > availableLen) { result += '...'; break; } first = false; result += `${formatValue(val, availableLen - result.length)}`; } result += ' ]'; return result; } function formatObject(value: object, availableLen: number): string { if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) { const val = value.toString(); if (val.length <= availableLen) { return val; } return val.substring(0, availableLen - 3) + '...'; } let result = '{ '; let first = true; for (const [key, val] of Object.entries(value)) { if (!first) { result += ', '; } if (result.length - 5 > availableLen) { result += '...'; break; } first = false; result += `${key}: ${formatValue(val, availableLen - result.length)}`; } result += ' }'; return result; } function repeat(str: string, count: number): string { let result = ''; for (let i = 1; i <= count; i++) { result += str; } return result; } function padStr(str: string, length: number): string { while (str.length < length) { str += ' '; } return str; }