UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

512 lines 20.5 kB
"use strict"; /* * This file contains all our utilities for dealing with Mermaid-js */ Object.defineProperty(exports, "__esModule", { value: true }); exports.mermaidEscape = exports.COLORS = void 0; exports.planToMermaid = planToMermaid; const tslib_1 = require("tslib"); const stripAnsi_js_1 = require("./stripAnsi.js"); /** * An array of hex colour codes that we use for colouring the buckets/steps in * the mermaid-js plan diagram. * * Generated by mokole.com/palette.html; re-ordered by Jem */ exports.COLORS = [ "#696969", "#00bfff", "#7f007f", "#ffa500", "#0000ff", "#7fff00", "#ff1493", "#808000", "#dda0dd", "#ff0000", "#ffff00", "#00ffff", "#4169e1", "#3cb371", "#a52a2a", "#ff00ff", "#f5deb3", ]; /** * Given a string, escapes the string so that it can be embedded as the description of a node in a mermaid chart. * * 1. If it's already safe, returns it verbatim * 2. If it contains disallowed characters, escape them by replacing them with similar-looking characters, * 3. Wrap the string in quote marks. * * @remarks * * NOTE: rather than doing literal escapes, we replace with lookalike characters because: * * 1. Mermaid has a bug when calculating the width of the node that doesn't respect escapes, * 2. It's easier to read the raw mermaid markup with substitutes rather than messy escapes. * * @internal */ const mermaidEscape = (str) => { if (str.match(/^[a-z0-9 ]+$/i)) { return str; } // Technically we should replace with escapes like this: //.replace(/[#"]/g, (l) => ({ "#": "#35;", '"': "#quot;" }[l as any])) // However there's a bug in Mermaid's rendering that causes the node to use // the escaped string as the width for the node rather than the unescaped // string. Thus we replace with similar looking characters. return `"${(0, stripAnsi_js_1.stripAnsi)(str.trim()) .replace(/[#"<>]/g, (l) => ({ "#": "ꖛ", '"': "”", "<": "ᐸ", ">": "ᐳ" })[l]) .replace(/\r?\n/g, "<br />")}"`; }; exports.mermaidEscape = mermaidEscape; function isGrafastPlanJSONv1(json) { return json.version === "v1"; } function planToMermaid(planJSON, { // printPathRelations = false, concise = false, skipBuckets = global.grafastExplainMermaidSkipBuckets ?? false, } = {}) { if (!isGrafastPlanJSONv1(planJSON)) { throw new Error("planToMermaid only supports v1 plan JSON"); } const stepById = Object.create(null); const layerPlanById = Object.create(null); const dependentsByStepId = Object.create(null); const sortedSteps = []; const extractSteps = (bucket) => { if (layerPlanById[bucket.id]) { throw new Error(`Two buckets with the same ID ${bucket.id}?`); } layerPlanById[bucket.id] = bucket; // Shallowest bucket first, then most dependencies const sorted = [...bucket.steps].sort((a, z) => z.dependencyIds.length - a.dependencyIds.length); for (const step of sorted) { if (stepById[step.id]) { throw new Error(`Step ${step.id} (${step.stepClass}/${step.metaString}) duplicated in plan?!`); } stepById[step.id] = step; sortedSteps.push(step); for (const depId of step.dependencyIds) { if (!dependentsByStepId[depId]) { dependentsByStepId[depId] = [step]; } else { dependentsByStepId[depId].push(step); } } } }; planJSON.buckets.forEach(extractSteps); const shouldHideStep = (step) => { if (step.stepClass === "ConstantStep") return true; if (step.extra?.constant?.type === "undefined") { return true; } if (step.bucketId === 0 && step.isSyncAndSafe && (dependentsByStepId[step.id]?.length ?? 0) === 0) { return true; } return false; }; const partitionToPoly = (bucketId) => { const lp = layerPlanById[bucketId]; if (lp.reason.type === "polymorphicPartition") { return layerPlans.find((lp) => lp.childIds.includes(bucketId)); } else { return bucketId; } }; const isExcessivelyReferenced = (stepId) => { const step = stepById[stepId]; if (!step) return true; if (step.stepClass === "ConstantStep") return true; if (step.bucketId !== 0) return false; const dependents = dependentsByStepId[stepId]; if (!dependents || dependents.length === 0) return false; const firstDependent = dependents[0]; // If it's referenced by more than one layer plan, it's excessive return dependents.some((d) => partitionToPoly(d.bucketId) !== partitionToPoly(firstDependent.bucketId)); }; const color = (i) => { return exports.COLORS[i % exports.COLORS.length]; }; const planStyle = `fill:#fff,stroke-width:1px,color:#000`; const itemplanStyle = `fill:#fff,stroke-width:2px,color:#000`; const unbatchedplanStyle = `fill:#dff,stroke-width:1px,color:#000`; const sideeffectplanStyle = `fill:#fcc,stroke-width:2px,color:#000`; const graph = [ `%%{init: {'themeVariables': { 'fontSize': '12px'}}}%%`, `${concise ? "flowchart" : "graph"} TD`, ` classDef path fill:#eee,stroke:#000,color:#000`, ` classDef plan ${planStyle}`, ` classDef itemplan ${itemplanStyle}`, ` classDef unbatchedplan ${unbatchedplanStyle}`, ` classDef sideeffectplan ${sideeffectplanStyle}`, ` classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left`, ``, ]; const squish = (str, start = 8, end = 8) => { if (str.length > start + end + 4) { return `${str.slice(0, start)}...${str.slice(str.length - end)}`; } return str; }; const stepToString = (step) => { return `${step.stepClass.replace(/Step$/, "")}${step.bucketId === 0 ? "" : `{${step.bucketId}}`}${step.metaString ? `<${step.metaString}>` : ""}[${step.id}]`; }; const layerPlans = Object.values(layerPlanById); if (!skipBuckets) { if (!concise) graph.push(" subgraph Buckets"); for (let i = 0, l = layerPlans.length; i < l; i++) { const layerPlan = layerPlans[i]; const raisonDEtre = ` (${layerPlan.reason.type})` + (layerPlan.reason.type === "polymorphicPartition" ? `\n|${layerPlan.reason.typeNames.join("\n|")}` : layerPlan.reason.type === "polymorphic" ? `\n__typename: ${shortStep(stepById[layerPlan.reason.parentStepId])}` : ``); graph.push(` Bucket${layerPlan.id}(${(0, exports.mermaidEscape)(`Bucket ${layerPlan.id}${raisonDEtre}${layerPlan.parentSideEffectStepId != null ? `\nParent side effect step: ${stepById[layerPlan.parentSideEffectStepId].id}` : ""}${layerPlan.copyStepIds.length > 0 ? `\nDeps: ${layerPlan.copyStepIds .map((pId) => stepById[pId].id) .join(", ")}` : ""}${layerPlan.reason.type === "polymorphicPartition" ? "\n" + pp(layerPlan.reason.polymorphicPaths) : ""}\n${layerPlan.rootStepId != null && layerPlan.reason.type !== "root" ? `\nROOT ${stepToString(stepById[layerPlan.rootStepId])}` : ""}${startSteps(layerPlan)}`)}):::bucket`); } if (!concise) graph.push(" end"); } if (!skipBuckets) { for (let i = 0, l = layerPlans.length; i < l; i++) { const layerPlan = layerPlans[i]; const childNodes = layerPlan.childIds.map((cId) => `Bucket${cId}`); if (childNodes.length > 0) { graph.push(` Bucket${layerPlan.id} --> ${childNodes.join(" & ")}`); } } } const planIdMap = Object.create(null); const planId = (plan) => { if (!planIdMap[plan.id]) { if (shouldHideStep(plan)) { console.warn(`Was expecting to hide step ${plan.id}, but we're rendering it anyway?`); } const planName = plan.stepClass.replace(/Step$/, ""); const planNode = `${planName}${plan.id}`; planIdMap[plan.id] = planNode; const rawMeta = plan.metaString; const strippedMeta = rawMeta != null ? (0, stripAnsi_js_1.stripAnsi)(rawMeta) : null; const meta = concise && strippedMeta ? squish(strippedMeta) : strippedMeta; const isUnbatched = plan.supportsUnbatched; const polyPaths = pp(plan.polymorphicPaths); const depIdsInSameBucket = plan.dependencyIds.filter((id) => stepById[id].bucketId === plan.bucketId); const polyPathsAreSame = depIdsInSameBucket.length >= 1 && depIdsInSameBucket.every((id) => pp(stepById[id].polymorphicPaths) === polyPaths); const polyPathsIfDifferent = polyPathsAreSame || polyPaths === "" ? "" : `\n${polyPaths}`; const [lBrace, rBrace, shape = "", planNameText = planName] = plan.stepClass === "__ItemStep" ? ["[/", "\\]"] : plan.stepClass === "__ValueStep" && plan.extra?.combined ? [null, null, "docs", "__Combined"] : plan.isSyncAndSafe ? isUnbatched ? ["{{", "}}"] : ["[", "]"] : ["[[", "]]"]; const depCount = isExcessivelyReferenced(plan.id) ? `\nDependents: ${dependentsByStepId[plan.id]?.length}` : ``; const extraDeps = plan.dependencyIds.filter(isExcessivelyReferenced); const extraDependencies = extraDeps.length > 0 ? `\nMore deps:\n${extraDeps .map((depId) => { const step = stepById[depId]; return `- ${shortStep(step)}`; }) .join("\n")}` : ``; const planString = `${planNameText}[${plan.id}${`∈${plan.bucketId}`}${plan.stream ? "@s" : ""}]${plan.isUnary ? " ➊" : ""}${polyPathsAreSame && polyPaths !== "" ? "^" : ""}${meta ? `\n<${meta}>` : ""}${polyPathsIfDifferent}${depCount}${extraDependencies}`; const planClass = plan.hasSideEffects ? "sideeffectplan" : plan.stepClass === "__ItemStep" ? "itemplan" : isUnbatched && !plan.isSyncAndSafe ? "unbatchedplan" : "plan"; if (lBrace === null) { graph.push(` ${planNode}:::${planClass}@{shape: ${shape}, label: ${(0, exports.mermaidEscape)(planString)}}`); } else { graph.push(` ${planNode}${lBrace}${(0, exports.mermaidEscape)(planString)}${rBrace}:::${planClass}`); } } return planIdMap[plan.id]; }; graph.push(""); graph.push(" %% plan dependencies"); const chainByDep = Object.create(null); const depDeets = (step, idx) => { const forbiddenFlags = step.dependencyForbiddenFlags[idx]; const onReject = step.dependencyOnReject[idx]; // TODO: factor 'data only' into the diagrams // // const dataOnly = step.dependencyDataOnly[idx]; const info = []; if (forbiddenFlags) { if ((forbiddenFlags & 2) === 2) { info.push("rejectNull"); } if ((forbiddenFlags & 1) === 0) { info.push("trapError"); } if ((forbiddenFlags & 4) === 0) { info.push("trapInhibited"); } if (onReject) { info.push(`onReject=${trim((0, stripAnsi_js_1.stripAnsi)(onReject))}`); } } const str = info.join(";"); if (str) { return `|${(0, exports.mermaidEscape)(str)}|`; } else { return ""; } }; sortedSteps.forEach( // This comment is here purely to maintain the previous formatting to reduce a git diff. (plan) => { if (shouldHideStep(plan)) return; const planNode = planId(plan); const depNodes = plan.dependencyIds.map((depId) => { const step = stepById[depId]; if (shouldHideStep(step) || isExcessivelyReferenced(depId)) { return null; } else { return planId(step); } }); const transformItemPlanNode = null; /* plan.stepClass === '__ListTransformStep' ? planId( steps[operationPlan.transformDependencyPlanIdByTransformStepId[plan.id]], ) : null; */ const firstDep = depNodes.find((n) => n != null); if (depNodes.length > 0 && firstDep) { if (plan.stepClass === "__ItemStep") { const arrow = plan.extra?.transformStepId == null ? "==>" : "-.->"; graph.push(` ${firstDep} ${arrow}${depDeets(plan, 0)} ${planNode}`); const normal = []; for (let i = 0; i < depNodes.length; i++) { const r = depNodes[i]; if (r == null || r === firstDep) continue; const deets = depDeets(plan, i); if (deets) { graph.push(` ${r} -->${deets} ${planNode}`); } else { normal.push(r); } } if (normal.length > 0) { outputGroupedNormalLinks(graph, planNode, normal); } } else { if (concise && !dependentsByStepId[plan.id] && depNodes.length === 1) { // Try alternating the nodes so they render closer together if (chainByDep[firstDep] === undefined) { graph.push(` ${firstDep} -->${depDeets(plan, 0)} ${planNode}`); } else { graph.push(` ${chainByDep[firstDep]} o--o${depDeets(plan, 0)} ${planNode}`); } chainByDep[firstDep] = planNode; } else { const normal = []; for (let i = 0; i < depNodes.length; i++) { const r = depNodes[i]; if (r == null) continue; const deets = depDeets(plan, i); if (deets) { graph.push(` ${r} -->${deets} ${planNode}`); } else { normal.push(r); } } outputGroupedNormalLinks(graph, planNode, normal); } } } if (transformItemPlanNode) { graph.push(` ${transformItemPlanNode} -.-> ${planNode}`); } return plan; }); graph.push(""); graph.push(" %% define steps"); sortedSteps.forEach((step) => { if (!shouldHideStep(step)) { planId(step); } }); for (let i = 0, l = layerPlans.length; i < l; i++) { const layerPlan = layerPlans[i]; const steps = layerPlan.steps; graph.push(` classDef bucket${layerPlan.id} stroke:${color(typeof layerPlan.id === "number" ? layerPlan.id : Object.values(layerPlanById).indexOf(layerPlan))}`); graph.push(` class ${[ `Bucket${layerPlan.id}`, ...steps.filter((s) => !shouldHideStep(s)).map(planId), ].join(",")} bucket${layerPlan.id}`); } let firstSideEffect = true; const recursivelyDependsOnSideEffect = (dependentId, sideEffectId) => { const step = stepById[dependentId]; return step.dependencyIds.some((stepId) => stepId === sideEffectId || stepById[stepId].implicitSideEffectStepId === sideEffectId || recursivelyDependsOnSideEffect(stepId, sideEffectId)); }; sortedSteps.forEach((step) => { if (step.implicitSideEffectStepId) { // Only add it if our parent doesn't already have it if (!recursivelyDependsOnSideEffect(step.id, step.implicitSideEffectStepId)) { if (firstSideEffect) { graph.push(""); graph.push(" %% implicit side effects"); firstSideEffect = false; } const sideEffectStep = stepById[step.implicitSideEffectStepId]; graph.push(` ${planId(sideEffectStep)} -.-o ${planId(step)}`); } } }); graph.push(""); // Handle concat for any "combination" layer plans for (let i = 0, l = layerPlans.length; i < l; i++) { const layerPlan = layerPlans[i]; if (layerPlan.reason.type === "combined") { for (const { targetStepId, sources } of layerPlan.reason.combinations) { const targetStep = stepById[targetStepId]; for (const { stepId } of sources) { const step = stepById[stepId]; graph.push(` ${planId(step)} -.-x ${planId(targetStep)}`); } } } } const graphString = graph.join("\n"); return graphString; function startSteps(layerPlan) { return layerPlan.phases.length === 1 ? `` : `\n${layerPlan.phases .map((phase, i) => `${i + 1}: ${shortSteps(phase.normalStepIds?.map((id) => stepById[id]))}${phase.unbatchedStepIds ? `\n>: ${shortSteps(phase.unbatchedStepIds.map((id) => stepById[id]))}` : ""}`) .join("\n")}`; } } function shortStep(step) { if (step.stepClass === "ConstantStep") { return `Constant${step.metaString ? `<${step.metaString}>` : ""}[${step.id}]`; } return `${step.stepClass.replace(/Step$/, "") ?? ""}[${step.id}]`; } function shortSteps(steps) { if (!steps) { return ""; } const str = steps.map(shortStep).join(", "); if (str.length < 40) { return str; } else { return steps.map((s) => s.id).join(", "); } } function pp(polymorphicPaths) { if (polymorphicPaths == null) { return ""; } const counts = Object.create(null); const out = []; const last = Object.create(null); for (const p of polymorphicPaths) { const i = p.lastIndexOf(">"); if (i <= 0) { out.push(p); } else { const end = p.slice(i); last[end] = p; if (counts[end]) { counts[end]++; } else { counts[end] = 1; } } } for (const [end, count] of Object.entries(counts)) { out.push(count === 1 ? last[end] : `(${count}) ...${end}`); } return out.join("\n"); } tslib_1.__exportStar(require("./planJSONInterfaces.js"), exports); function trim(string, length = 15) { if (string.length > length) { return string.substring(0, length - 2) + "…"; } else { return string; } } function outputGroupedNormalLinks(graph, planNode, normal) { if (normal.length) { const counts = Object.create(null); for (const n of normal) { if (counts[n]) { counts[n]++; } else { counts[n] = 1; } } const oners = Object.keys(counts).filter((n) => counts[n] === 1); if (oners.length >= 1) { graph.push(` ${oners.join(" & ")} --> ${planNode}`); } for (const [n, c] of Object.entries(counts)) { if (c !== 1) { graph.push(` ${n} -- ${c} --> ${planNode}`); } } } } //# sourceMappingURL=mermaid.js.map