@discoveryjs/discovery
Version:
Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards
232 lines (231 loc) • 7.17 kB
JavaScript
import { hasOwn } from "./utils/object-utils.js";
import { Dictionary } from "./dict.js";
const warnings = /* @__PURE__ */ new Map();
let groupWarningsTimer = null;
function flushWarnings(logger) {
groupWarningsTimer = null;
for (const [caption, messages] of warnings.entries()) {
logger.warn.groupCollapsed(`${caption} (${messages.length})`, messages);
}
warnings.clear();
}
function groupWarning(logger, caption, ...details) {
if (groupWarningsTimer === null && warnings.size === 0) {
groupWarningsTimer = setTimeout(() => flushWarnings(logger), 1);
}
const warningsGroup = warnings.get(caption);
if (warningsGroup !== void 0) {
warningsGroup.push(details);
} else {
warnings.set(caption, [details]);
}
}
function getter(name, getter2, reference) {
switch (typeof getter2) {
case "function":
return getter2;
case "string":
return Object.assign(
(object) => object && hasOwn(object, getter2) ? object[getter2] : void 0,
{ getterFromString: `object[${JSON.stringify(getter2)}]` }
);
default:
throw new Error(`[Discovery] Bad type "${typeof getter2}" for ${reference} in object marker "${name}" config (must be a string or a function)`);
}
}
function configGetter(name, config, property, fallback) {
const value = config && hasOwn(config, property) ? config[property] : void 0;
if (value !== void 0) {
return getter(name, value, `"${property}" option`);
}
return fallback;
}
function configArrayGetter(name, config, property) {
const array = Array.isArray(config[property]) ? config[property] || [] : [];
return array.map(
(key) => getter(name, key, `"${property}" option`)
);
}
function isLookupValue(value, objectsOnly = false) {
if (value === null) {
return false;
}
return objectsOnly ? typeof value === "object" : typeof value === "object" || typeof value === "number" || typeof value === "string";
}
function createObjectMarker(logger, config) {
const {
name,
indexRefs,
lookupRefs,
annotateScalars,
page,
getRef,
getTitle
} = config;
if (getRef !== null) {
indexRefs.unshift(getRef);
}
if (page && getRef === null) {
logger.warn(`Option "ref" for "${name}" marker must be specified when "page" options is defined ("page" option ignored)`);
}
if (indexRefs.length > 0) {
lookupRefs.unshift((value) => value);
}
const markedObjects = /* @__PURE__ */ new Set();
const indexedRefs = /* @__PURE__ */ new Map();
const markers = /* @__PURE__ */ new Map();
let weakRefs = /* @__PURE__ */ new WeakMap();
const reset = () => {
markedObjects.clear();
indexedRefs.clear();
markers.clear();
weakRefs = /* @__PURE__ */ new WeakMap();
};
const mark = (object) => {
if (object === null || typeof object !== "object") {
logger.warn(`Invalid value used for "${name}" marker (should be an object)`);
return;
}
markedObjects.add(object);
for (const indexRefGetter of indexRefs) {
const ref = indexRefGetter(object);
if (isLookupValue(ref)) {
if (!indexedRefs.has(ref)) {
indexedRefs.set(ref, object);
continue;
}
if (indexedRefs.get(ref) !== object) {
groupWarning(logger, `The same reference value used for different objects for "${name}" marker`, `Reference value "${ref}"`, {
refGetter: indexRefGetter.getterFromString || indexRefGetter,
ref,
currentObject: indexedRefs.get(ref),
newObject: object
});
}
}
}
};
const lookup = (value, useAnnotateScalars = false) => {
if (!isLookupValue(value, useAnnotateScalars ? !annotateScalars : false)) {
return null;
}
const isObject = typeof value === "object";
const knownDescriptor = isObject ? weakRefs.get(value) : markers.get(value);
if (knownDescriptor !== void 0) {
return knownDescriptor;
}
let resolvedObject = null;
if (isObject && markedObjects.has(value)) {
resolvedObject = value;
} else {
for (const getLookupRef of lookupRefs) {
const ref2 = getLookupRef(value);
const candidate = indexedRefs.get(ref2);
if (candidate !== void 0) {
resolvedObject = candidate;
break;
}
}
}
if (resolvedObject === null) {
return null;
}
const markersDescriptor = markers.get(resolvedObject);
if (markersDescriptor !== void 0) {
return markersDescriptor;
}
const ref = getRef !== null ? getRef(resolvedObject) : null;
const newDescriptor = Object.freeze({
type: name,
object: resolvedObject,
ref,
title: getTitle(resolvedObject),
href: page !== null && ref !== null ? `#${encodeURIComponent(page)}:${encodeURIComponent(ref)}` : null
});
markers.set(resolvedObject, newDescriptor);
if (value !== resolvedObject) {
if (isObject) {
weakRefs.set(value, newDescriptor);
} else {
markers.set(value, newDescriptor);
}
}
return newDescriptor;
};
return {
name,
page: getRef !== null ? page : null,
mark,
lookup,
reset
};
}
export class ObjectMarkerManager extends Dictionary {
constructor(logger) {
super();
this.logger = logger;
}
#preventDefine = false;
lock() {
this.#preventDefine = true;
}
define(name, config) {
if (this.#preventDefine) {
throw new Error("Object marker definition is not allowed after setup");
}
if (this.isDefined(name)) {
throw new Error(`Object marker "${name}" is already defined, new definition ignored`);
}
config = config || {};
const indexRefs = configArrayGetter(name, config, "refs");
const lookupRefs = configArrayGetter(name, config, "lookupRefs");
const annotateScalars = Boolean(config.annotateScalars);
const configPage = config.page;
const page = typeof configPage === "string" ? configPage : null;
const getRef = configGetter(name, config, "ref", null);
const getTitle = configGetter(name, config, "title", getRef || (() => null));
return ObjectMarkerManager.define(this, name, Object.freeze(createObjectMarker(this.logger, {
name,
indexRefs,
lookupRefs,
annotateScalars,
page,
getRef,
getTitle
})));
}
reset() {
for (const { reset } of this.values) {
reset();
}
}
markerMap() {
return Object.fromEntries(
[...this.entries].map(([name, marker]) => [name, marker.mark])
);
}
// Returns first lookup match if marker is not specified
lookup(value, marker) {
if (typeof marker === "string") {
return this.get(marker)?.lookup(value) || null;
}
for (const { lookup } of this.values) {
const marker2 = lookup(value);
if (marker2 !== null) {
return marker2;
}
}
return null;
}
// Returns all lookup matches
lookupAll(value) {
const markers = [];
for (const { lookup } of this.values) {
const marker = lookup(value);
if (marker !== null) {
markers.push(marker);
}
}
return markers;
}
}