This commit is contained in:
Henning Dieterichs
2023-09-07 15:00:50 +02:00
parent d45dfdaa71
commit a9656e1e12
37 changed files with 5502 additions and 3664 deletions

View File

@@ -6,6 +6,5 @@ export function getGlobal(): any {
} else if (typeof window === "object") {
return window;
}
throw new Error("No global available");
}

View File

@@ -1,7 +1,4 @@
import {
VisualizationData,
DataExtractionResult,
} from "../../DataExtractionResult";
import { DataExtractionResult, VisualizationData } from "../../DataExtractionResult";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
export interface DataExtractorApi {
@@ -24,7 +21,8 @@ export interface DataExtractorApi {
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, unknown>
variablesInScope: Record<string, unknown>,
callFramesSnapshot: CallFramesSnapshot | null
): JSONString<DataResult>;
/**
@@ -42,12 +40,41 @@ export type DataResult =
extractionResult: DataExtractionResult;
}
| { kind: "NoExtractors" }
| { kind: "Error"; message: string };
| { kind: "Error"; message: string }
| {
kind: "OutdatedCallFrameSnapshot";
callFramesRequest: CallFramesRequest;
};
export interface JSONString<T> extends String {
__brand: { json: T };
}
export interface CallFramesRequest {
requestId: string;
requestedCallFrames: CallFrameRequest[];
}
export interface CallFrameRequest {
methodName: string;
pathRegExp: string | undefined;
}
export interface CallFramesSnapshot {
requestId: string;
frames: (CallFrameInfo | SkippedCallFrames)[];
}
export interface SkippedCallFrames {
skippedFrames: number;
}
export interface CallFrameInfo {
methodName: string;
source: { name: string; path: string };
vars: Record<string, any>;
}
export interface DataExtractor {
/**
* Must be unique among all data extractors.
@@ -58,11 +85,7 @@ export interface DataExtractor {
* Filters the data to be extracted.
*/
dataCtor?: string;
getExtractions(
data: unknown,
extractionCollector: ExtractionCollector,
context: DataExtractorContext
): void;
getExtractions(data: unknown, extractionCollector: ExtractionCollector, context: DataExtractorContext): void;
}
export interface ExtractionCollector {
@@ -78,11 +101,15 @@ export interface DataExtractorContext {
*/
evalFn: <TEval>(expression: string) => TEval;
expression: string | undefined;
readonly expression: string | undefined;
variablesInScope: Record<string, () => unknown>;
readonly variablesInScope: Record<string, () => unknown>;
extract(value: unknown): VisualizationData | undefined;
readonly callFrameInfos: readonly (CallFrameInfo | SkippedCallFrames)[];
addCallFrameRequest(request: CallFrameRequest): void;
}
export interface DataExtraction {

View File

@@ -1,19 +1,22 @@
import {
DataExtractorApi,
DataResult,
JSONString,
DataExtractor,
DataExtraction,
ExtractionCollector,
DataExtractorContext,
} from "./DataExtractorApi";
import {
DataExtractorInfo,
VisualizationData,
} from "../../DataExtractionResult";
import { registerDefaultExtractors } from "./default-extractors";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
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
@@ -42,51 +45,18 @@ export class DataExtractorApiImpl implements DataExtractorApi {
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, () => unknown>
variablesInScope: Record<string, () => unknown>,
callFramesSnapshot: CallFramesSnapshot | null
): JSONString<DataResult> {
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
) {}
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
)
);
if (extractions.length === 0) {
return undefined;
}
return extractions[0].extractData();
}
}
const callFrameRequests: CallFrameRequest[] = [];
const rootContext = new ContextImpl(
variablesInScope,
removeEnd(removeStart(valueFn.toString(), "() => ("), ")").trim(),
evalFn,
this,
undefined
undefined,
callFramesSnapshot?.frames ?? [],
callFrameRequests
);
DataExtractorApiImpl.lastContext = rootContext;
@@ -94,6 +64,20 @@ export class DataExtractorApiImpl implements DataExtractorApi {
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);
@@ -108,14 +92,6 @@ export class DataExtractorApiImpl implements DataExtractorApi {
}
}
function mapExtractor(e: DataExtraction): DataExtractorInfo {
return {
id: e.id! as any,
name: e.name!,
priority: e.priority,
};
}
const data = usedExtraction.extractData();
return this.toJson({
kind: "Data",
@@ -188,6 +164,60 @@ export class DataExtractorApiImpl implements DataExtractorApi {
}
}
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);
@@ -201,3 +231,20 @@ function removeEnd(str: string, end: string): string {
}
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);
}

View File

@@ -1,9 +1,9 @@
import * as fs from "fs";
import { getGlobal } from "../../getGlobal";
import * as globalHelpers from "../global-helpers";
import * as helpers from "../helpers";
import { DataExtractorApi } from "./DataExtractorApi";
import { DataExtractorApiImpl } from "./DataExtractorApiImpl";
import * as helpers from "../helpers";
import * as globalHelpers from "../global-helpers";
import { getGlobal } from "../../getGlobal";
/**
* Returns standalone JS code representing an expression that initializes the data extraction API.
@@ -50,8 +50,18 @@ export function getDataExtractorApi(): DataExtractorApi {
* @internal
*/
function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
const globalObj =
typeof window === "object" ? (window as any) : (global as any);
function getGlobal(): any {
if (typeof globalThis === "object") {
return globalThis;
} else if (typeof global === "object") {
return global;
} else if (typeof window === "object") {
return window;
}
throw new Error("No global available");
}
const globalObj = getGlobal();
const key: typeof apiKey = "@hediet/data-extractor-api/v3";
let api: DataExtractorApi | undefined = globalObj[key];
if (!api) {

View File

@@ -30,7 +30,12 @@ export function createGraphFromPointers<T>(
return createGraph<T | Pointer>(
items,
(item) => {
if ("marker" in item && item["marker"] === marker) {
if (
typeof item === "object" &&
item &&
"marker" in item &&
item["marker"] === marker
) {
return {
id: "label____" + item.name,
color: "orange",