UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

232 lines (231 loc) 7.17 kB
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; } }