295 lines
7.3 KiB
TypeScript
295 lines
7.3 KiB
TypeScript
import {
|
|
DataExtractorInfo,
|
|
VisualizationData,
|
|
} from "../../DataExtractionResult";
|
|
import * as helpers from "../helpers";
|
|
import {
|
|
CallFrameInfo,
|
|
CallFrameRequest,
|
|
CallFramesSnapshot,
|
|
DataExtraction,
|
|
DataExtractor,
|
|
DataExtractorApi,
|
|
DataExtractorContext,
|
|
DataResult,
|
|
JSONString,
|
|
SkippedCallFrames,
|
|
} from "./DataExtractorApi";
|
|
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
|
|
import { registerDefaultExtractors } from "./default-extractors";
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class DataExtractorApiImpl implements DataExtractorApi {
|
|
public static lastContext: DataExtractorContext | undefined = undefined;
|
|
|
|
private readonly extractors = new Map<string, DataExtractor>();
|
|
private readonly extractorSources = new Map<string, LoadDataExtractorsFn>();
|
|
|
|
private toJson<TData>(data: TData): JSONString<TData> {
|
|
return JSON.stringify(data) as any;
|
|
}
|
|
|
|
public registerExtractor(extractor: DataExtractor): void {
|
|
this.extractors.set(extractor.id, extractor);
|
|
}
|
|
|
|
public registerExtractors(extractors: DataExtractor[]): void {
|
|
for (const e of extractors) {
|
|
this.registerExtractor(e);
|
|
}
|
|
}
|
|
|
|
public getData(
|
|
valueFn: () => unknown,
|
|
evalFn: <T>(expression: string) => T,
|
|
preferredDataExtractorId: string | undefined,
|
|
variablesInScope: Record<string, () => unknown>,
|
|
callFramesSnapshot: CallFramesSnapshot | null
|
|
): JSONString<DataResult> {
|
|
const callFrameRequests: CallFrameRequest[] = [];
|
|
const rootContext = new ContextImpl(
|
|
variablesInScope,
|
|
removeEnd(removeStart(valueFn.toString(), "() => ("), ")").trim(),
|
|
evalFn,
|
|
this,
|
|
undefined,
|
|
callFramesSnapshot?.frames ?? [],
|
|
callFrameRequests
|
|
);
|
|
|
|
DataExtractorApiImpl.lastContext = rootContext;
|
|
const value = valueFn();
|
|
const extractions = this.getExtractions(value, rootContext);
|
|
DataExtractorApiImpl.lastContext = undefined;
|
|
|
|
const requestId =
|
|
callFrameRequests.length === 0
|
|
? ""
|
|
: "" + cyrb53(JSON.stringify(callFrameRequests));
|
|
if ((callFramesSnapshot?.requestId ?? "") !== requestId) {
|
|
return this.toJson({
|
|
kind: "OutdatedCallFrameSnapshot",
|
|
callFramesRequest: {
|
|
requestId,
|
|
requestedCallFrames: callFrameRequests,
|
|
},
|
|
});
|
|
}
|
|
|
|
let usedExtraction = extractions[0];
|
|
if (!usedExtraction) {
|
|
return this.toJson({ kind: "NoExtractors" } as DataResult);
|
|
}
|
|
|
|
if (preferredDataExtractorId) {
|
|
const preferred = extractions.find(
|
|
(e) => e.id === preferredDataExtractorId
|
|
);
|
|
if (preferred) {
|
|
usedExtraction = preferred;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const data = usedExtraction.extractData();
|
|
|
|
return this.toJson({
|
|
kind: "Data",
|
|
extractionResult: {
|
|
data,
|
|
usedExtractor: mapExtractor(usedExtraction),
|
|
availableExtractors: extractions.map(mapExtractor),
|
|
},
|
|
} as DataResult);
|
|
|
|
} catch (e) {
|
|
return this.toJson({
|
|
kind: "Data",
|
|
extractionResult: {
|
|
data: visualizeError(e),
|
|
usedExtractor: mapExtractor(usedExtraction),
|
|
availableExtractors: extractions.map(mapExtractor),
|
|
},
|
|
} as DataResult);
|
|
}
|
|
}
|
|
|
|
public getExtractions(
|
|
value: unknown,
|
|
context: DataExtractorContext
|
|
): DataExtraction[] {
|
|
const extractions = new Array<DataExtraction>();
|
|
const extractors = new Array<DataExtractor>();
|
|
|
|
for (const fn of this.extractorSources.values()) {
|
|
try {
|
|
fn((extractor) => {
|
|
extractors.push(extractor);
|
|
}, helpers);
|
|
} catch (e) {
|
|
console.error('Error in data extractor source', fn, e);
|
|
}
|
|
}
|
|
|
|
for (const extractor of [...this.extractors.values(), ...extractors]) {
|
|
if (extractor.dataCtor !== undefined) {
|
|
if (
|
|
typeof value !== "object" ||
|
|
value === null ||
|
|
value.constructor.name !== extractor.dataCtor
|
|
) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
try {
|
|
extractor.getExtractions(
|
|
value,
|
|
{
|
|
addExtraction(extraction) {
|
|
if (extraction.id === undefined) {
|
|
extraction.id = extractor.id;
|
|
}
|
|
if (extraction.name === undefined) {
|
|
extraction.name = extractor.id;
|
|
}
|
|
extractions.push(extraction);
|
|
},
|
|
},
|
|
context
|
|
);
|
|
} catch (e) {
|
|
extractions.push({
|
|
id: extractor.id,
|
|
name: extractor.id,
|
|
priority: 0,
|
|
extractData() {
|
|
return visualizeError(e);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
extractions.sort((a, b) => b.priority - a.priority);
|
|
|
|
return extractions;
|
|
}
|
|
|
|
public registerDefaultExtractors(preferExisting: boolean = false): void {
|
|
// TODO consider preferExisting
|
|
registerDefaultExtractors(this);
|
|
}
|
|
|
|
public setDataExtractorFn(
|
|
id: string,
|
|
fn: LoadDataExtractorsFn | undefined
|
|
): void {
|
|
if (fn) {
|
|
this.extractorSources.set(id, fn);
|
|
} else {
|
|
this.extractorSources.delete(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
function visualizeError(e: unknown | Error): VisualizationData {
|
|
return helpers.asData({
|
|
kind: { text: true },
|
|
fileName: 'error.md',
|
|
text: `# Error while running data extractor\n\n${formatErrorStr(e)}`,
|
|
});
|
|
}
|
|
|
|
function formatErrorStr(e: unknown | Error): string {
|
|
if (e instanceof Error) {
|
|
return `${e.message}\n\nStack:\n${e.stack}`;
|
|
}
|
|
return "" + e;
|
|
}
|
|
|
|
function mapExtractor(e: DataExtraction): DataExtractorInfo {
|
|
return {
|
|
id: e.id! as any,
|
|
name: e.name!,
|
|
priority: e.priority,
|
|
};
|
|
}
|
|
|
|
class ContextImpl implements DataExtractorContext {
|
|
constructor(
|
|
public readonly variablesInScope: Record<string, () => unknown>,
|
|
public readonly expression: string | undefined,
|
|
public readonly evalFn: <T>(expression: string) => T,
|
|
private readonly _api: DataExtractorApiImpl,
|
|
private readonly _parent: ContextImpl | undefined,
|
|
public readonly callFrameInfos: readonly (
|
|
| CallFrameInfo
|
|
| SkippedCallFrames
|
|
)[],
|
|
private readonly _callFrameRequests: CallFrameRequest[]
|
|
) { }
|
|
|
|
addCallFrameRequest(request: CallFrameRequest): void {
|
|
this._callFrameRequests.push(request);
|
|
}
|
|
|
|
get _level(): number {
|
|
return this._parent ? this._parent._level + 1 : 0;
|
|
}
|
|
|
|
extract(value: any): VisualizationData | undefined {
|
|
if (this._level > 10) {
|
|
throw new Error("extract() called too many times recursively");
|
|
}
|
|
|
|
const extractions = this._api.getExtractions(
|
|
value,
|
|
new ContextImpl(
|
|
this.variablesInScope,
|
|
undefined,
|
|
this.evalFn,
|
|
this._api,
|
|
this,
|
|
this.callFrameInfos,
|
|
this._callFrameRequests
|
|
)
|
|
);
|
|
if (extractions.length === 0) {
|
|
return undefined;
|
|
}
|
|
return extractions[0].extractData();
|
|
}
|
|
}
|
|
|
|
function removeStart(str: string, start: string): string {
|
|
if (str.startsWith(start)) {
|
|
return str.substr(start.length);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
function removeEnd(str: string, end: string): string {
|
|
if (str.endsWith(end)) {
|
|
return str.substr(0, str.length - end.length);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
// From https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
|
|
function cyrb53(str: string, seed = 0) {
|
|
let h1 = 0xdeadbeef ^ seed,
|
|
h2 = 0x41c6ce57 ^ seed;
|
|
for (let i = 0, ch; i < str.length; i++) {
|
|
ch = str.charCodeAt(i);
|
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
}
|
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
|
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
}
|