grafast
Version:
Cutting edge GraphQL planning and execution engine
512 lines • 20.5 kB
JavaScript
"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