@webpieces/nx-webpieces-rules 0.4.528 → 0.4.530
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/lib/api-usage/api-ast.d.ts +29 -1
- package/src/lib/api-usage/api-ast.js +79 -1
- package/src/lib/api-usage/api-ast.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +15 -1
- package/src/lib/api-usage/api-contract-errors.js +30 -1
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +42 -0
- package/src/lib/api-usage/api-relations.js +31 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +8 -2
- package/src/lib/api-usage/api-scanner.js +8 -1
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +12 -0
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph-sorters.d.ts +26 -0
- package/src/lib/runtime-graph-sorters.js +66 -0
- package/src/lib/runtime-graph-sorters.js.map +1 -0
- package/src/lib/runtime-graph.js +12 -51
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.client.js +22 -0
- package/src/lib/runtime-visualizer.d.ts +10 -4
- package/src/lib/runtime-visualizer.js +206 -174
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/lib/runtime-viz-theme.d.ts +61 -0
- package/src/lib/runtime-viz-theme.js +137 -0
- package/src/lib/runtime-viz-theme.js.map +1 -0
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* Shape says what a node IS and line style says what a call IS. Solid = rpc: the
|
|
17
17
|
* request follows the arrow and the response flows back. Dashed = event: it flows
|
|
18
18
|
* in the arrow's direction and returns once it is queued. A queue is a sideways
|
|
19
|
-
* cylinder, a datastore an upright one.
|
|
19
|
+
* cylinder, a datastore an upright one. A queue box lists ONE LINE PER QUEUE: the
|
|
20
|
+
* unit underneath is still the METHOD (what Terraform creates, and what
|
|
21
|
+
* runtime-dependencies.json records), but queues of one contract sharing the same
|
|
22
|
+
* producers and consumers are drawn together instead of as adjacent near-identical
|
|
23
|
+
* boxes — every queue is still named, only the node count drops.
|
|
20
24
|
*
|
|
21
25
|
* Calls that leave the repo (a contract NOTHING in-repo implements — firestore,
|
|
22
26
|
* gmail, ...) are drawn as terminal nodes, so the vendor systems that actually
|
|
@@ -26,9 +30,11 @@
|
|
|
26
30
|
* derivation, levels and cycle detection never see them.
|
|
27
31
|
*
|
|
28
32
|
* The same is true in the other direction for endpoints nothing in-repo CALLS: a
|
|
29
|
-
* `cron` method hangs off a clock and an `external` method off a
|
|
30
|
-
*
|
|
31
|
-
*
|
|
33
|
+
* `cron` method hangs off a clock and an `external` method off a box naming the
|
|
34
|
+
* CALLER that posts to it (`twilio`), in the same id space as the outbound
|
|
35
|
+
* systems, so a vendor we both call and are called by is ONE box. Those are the
|
|
36
|
+
* entry points that wake a service up at 3am, and a graph built only from in-repo
|
|
37
|
+
* callers cannot show them at all.
|
|
32
38
|
*/
|
|
33
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
40
|
exports.RuntimeVizOptions = void 0;
|
|
@@ -38,64 +44,7 @@ const tslib_1 = require("tslib");
|
|
|
38
44
|
const fs = tslib_1.__importStar(require("fs"));
|
|
39
45
|
const path = tslib_1.__importStar(require("path"));
|
|
40
46
|
const dot_syntax_1 = require("./dot-syntax");
|
|
41
|
-
const
|
|
42
|
-
0: '#E8F5E9',
|
|
43
|
-
1: '#E3F2FD',
|
|
44
|
-
2: '#FFF3E0',
|
|
45
|
-
3: '#FCE4EC',
|
|
46
|
-
};
|
|
47
|
-
const QUEUE_FILL = '#FFF3E0';
|
|
48
|
-
/**
|
|
49
|
-
* The queue node is an `Mrecord` whose FIRST field is empty, which draws a rounded outline with a
|
|
50
|
-
* vertical cap line near one end — a cylinder lying on its side, distinguishing a queue from the
|
|
51
|
-
* upright cylinder that now means a database.
|
|
52
|
-
*
|
|
53
|
-
* Two things here are load-bearing and easy to break:
|
|
54
|
-
*
|
|
55
|
-
* 1. NO surrounding `{}`. Record fields lay out along the rank direction, and this graph is
|
|
56
|
-
* `rankdir=TB` (see {@link generateRuntimeDot}), where the default is horizontal — which is what
|
|
57
|
-
* we want. Adding braces TOGGLES that, turning the cap line into a band across the top.
|
|
58
|
-
* 2. The leading space is the empty field. The record parser trims it to nothing, which is the
|
|
59
|
-
* point; it must survive as its own field, so the `|` cannot be dropped.
|
|
60
|
-
*
|
|
61
|
-
* Graphviz has no sideways cylinder and never has: `orientation=` is documented as rotating POLYGON
|
|
62
|
-
* shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute (graphviz issue
|
|
63
|
-
* #2244, open since 2022 and still reproducible on 13.0.0). This is the closest native shape.
|
|
64
|
-
*/
|
|
65
|
-
const QUEUE_SHAPE = 'Mrecord';
|
|
66
|
-
const QUEUE_LABEL_PREFIX = ' |';
|
|
67
|
-
/**
|
|
68
|
-
* Marker class stamped on every queue node. Graphviz copies `class` straight into the rendered
|
|
69
|
-
* `<g class="node wp_queue">`, which is how runtime-visualizer.client.js finds these nodes and
|
|
70
|
-
* redraws them as true horizontal cylinders in the browser.
|
|
71
|
-
*
|
|
72
|
-
* A CLASS rather than an id prefix, because queue-kind EXTERNAL systems are queues too and share the
|
|
73
|
-
* `system__` id space with databases — which must stay upright. Underscored, not hyphenated: DOT
|
|
74
|
-
* emits a hyphen as `-`, which is harmless but needlessly surprising to anyone reading the SVG.
|
|
75
|
-
*/
|
|
76
|
-
const QUEUE_CLASS = 'wp_queue';
|
|
77
|
-
/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */
|
|
78
|
-
const DATABASE_FILL = '#E1F5FE';
|
|
79
|
-
/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */
|
|
80
|
-
const EXTERNAL_SHAPES = {
|
|
81
|
-
database: 'cylinder',
|
|
82
|
-
cache: 'cylinder',
|
|
83
|
-
queue: 'Mrecord',
|
|
84
|
-
storage: 'folder',
|
|
85
|
-
};
|
|
86
|
-
/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */
|
|
87
|
-
const EXTERNAL_FILLS = {
|
|
88
|
-
database: DATABASE_FILL,
|
|
89
|
-
cache: DATABASE_FILL,
|
|
90
|
-
queue: QUEUE_FILL,
|
|
91
|
-
storage: '#F3E5F5',
|
|
92
|
-
};
|
|
93
|
-
/** Fill + border for the dashed terminal node standing for a system outside this repo. */
|
|
94
|
-
const EXTERNAL_FILL = '#FAFAFA';
|
|
95
|
-
const EXTERNAL_BORDER = '#9E9E9E';
|
|
96
|
-
/** Fill + border for the clock node standing for a scheduler-driven endpoint. */
|
|
97
|
-
const CRON_FILL = '#FFF9C4';
|
|
98
|
-
const CRON_BORDER = '#F9A825';
|
|
47
|
+
const runtime_viz_theme_1 = require("./runtime-viz-theme");
|
|
99
48
|
/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */
|
|
100
49
|
const APIS_PER_LABEL_LINE = 3;
|
|
101
50
|
/** Separator for the (service, external-library) grouping key; illegal in both project names. */
|
|
@@ -166,43 +115,171 @@ function nodeLabel(name, svc) {
|
|
|
166
115
|
return label;
|
|
167
116
|
}
|
|
168
117
|
/**
|
|
169
|
-
* DOT for ONE runtime edge
|
|
170
|
-
* comes back). pubsub
|
|
171
|
-
*
|
|
118
|
+
* DOT for ONE NON-QUEUED runtime edge: a direct labeled SOLID arrow (producer calls consumer, the
|
|
119
|
+
* response comes back). Queued (pubsub) hops are drawn by {@link queuesDot} instead, because they
|
|
120
|
+
* are merged across methods and therefore cannot be emitted one edge at a time.
|
|
172
121
|
*
|
|
173
122
|
* Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a
|
|
174
123
|
* response, dashed is an event that returns as soon as it is queued.
|
|
175
124
|
*/
|
|
176
125
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
177
|
-
function edgeDot(edge
|
|
126
|
+
function edgeDot(edge) {
|
|
178
127
|
const from = (0, dot_syntax_1.dotValue)(getShortName(edge.from));
|
|
179
128
|
const to = (0, dot_syntax_1.dotValue)(getShortName(edge.to));
|
|
180
|
-
// Kept RAW: an ordinary edge label needs dotValue, the record-mode queue label needs
|
|
181
|
-
// recordValue, and recordValue already applies dotValue — escaping here would double it.
|
|
182
129
|
const viaRaw = edge.via.map((v) => getShortName(v)).join(', ');
|
|
183
|
-
|
|
184
|
-
|
|
130
|
+
return ` "${from}" -> "${to}" [label="${(0, dot_syntax_1.dotValue)(viaRaw)}"];\n`;
|
|
131
|
+
}
|
|
132
|
+
/** The VISIBLE producers and consumers of one queue, gathered from the edges that survived hiding. */
|
|
133
|
+
class QueueEndpoints {
|
|
134
|
+
producers = new Set();
|
|
135
|
+
consumers = new Set();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One drawn queue BOX: the queue keys (`Api.method`) it shows, and the endpoints they all share.
|
|
139
|
+
* Several keys land in one box only when they agree on all three of contract, producers and
|
|
140
|
+
* consumers — see {@link queuesDot}.
|
|
141
|
+
*/
|
|
142
|
+
class QueueGroup {
|
|
143
|
+
members;
|
|
144
|
+
producers;
|
|
145
|
+
consumers;
|
|
146
|
+
constructor(members, producers, consumers) {
|
|
147
|
+
this.members = members;
|
|
148
|
+
this.producers = producers;
|
|
149
|
+
this.consumers = consumers;
|
|
185
150
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The label line for ONE queue inside a box: `Api.method`, and the Terraform queue name ONLY when it
|
|
154
|
+
* is not the derived `${Api}-${method}`.
|
|
155
|
+
*
|
|
156
|
+
* Printing a derived name was pure restatement — the line above it already said `Api.method`, so
|
|
157
|
+
* every queue box carried a second line that added nothing and doubled its height. A `@Queue(...)`
|
|
158
|
+
* OVERRIDE is the opposite: that string appears nowhere else on the graph, and it is the one string
|
|
159
|
+
* Terraform must match, so it stays.
|
|
160
|
+
*/
|
|
161
|
+
// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
|
|
162
|
+
function queueLine(key, queue) {
|
|
163
|
+
const line = (0, dot_syntax_1.recordValue)(key);
|
|
164
|
+
if (queue === undefined)
|
|
165
|
+
return line;
|
|
166
|
+
if (queue.queueName === `${queue.api}-${queue.method}`)
|
|
167
|
+
return line;
|
|
168
|
+
return `${line}\\nqueue: ${(0, dot_syntax_1.recordValue)(queue.queueName)}`;
|
|
169
|
+
}
|
|
170
|
+
/** The node statement + enqueue/deliver arrows for one queue box. Names arrive pre-escaped. */
|
|
171
|
+
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
172
|
+
function queueBoxDot(id, body, producers, consumers) {
|
|
191
173
|
// Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the
|
|
192
|
-
// empty leading field that draws the cylinder's end cap.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
`
|
|
198
|
-
|
|
199
|
-
` "${
|
|
174
|
+
// empty leading field that draws the cylinder's end cap. Drop it and the node silently
|
|
175
|
+
// degrades to a plain box.
|
|
176
|
+
let dot = ` "${id}" [shape=${runtime_viz_theme_1.QUEUE_SHAPE}, style="filled", fillcolor="${runtime_viz_theme_1.QUEUE_FILL}", ` +
|
|
177
|
+
`class="${runtime_viz_theme_1.QUEUE_CLASS}", label="${runtime_viz_theme_1.QUEUE_LABEL_PREFIX}${body}"];\n`;
|
|
178
|
+
for (const producer of producers)
|
|
179
|
+
dot += ` "${producer}" -> "${id}" [label="enqueue", style=dashed];\n`;
|
|
180
|
+
for (const consumer of consumers)
|
|
181
|
+
dot += ` "${id}" -> "${consumer}" [label="deliver", style=dashed];\n`;
|
|
182
|
+
return dot;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Group the queued hops into boxes. The key is (contract, producer SET, consumer SET) — NOT the
|
|
186
|
+
* (from,to) pair of some edge.
|
|
187
|
+
*
|
|
188
|
+
* A queue node converges EVERY producer and consumer of its method onto one box, so keying by a
|
|
189
|
+
* service PAIR would re-split a two-producer queue into two boxes and invent topology nobody wrote.
|
|
190
|
+
* Only queues of the same contract whose producer and consumer sets are IDENTICAL may share a box;
|
|
191
|
+
* anything else stays separate, because merging it would assert a routing that does not exist.
|
|
192
|
+
*/
|
|
193
|
+
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
194
|
+
function groupQueues(byQueue, graph) {
|
|
195
|
+
const groups = new Map();
|
|
196
|
+
// Sorted keys → the first member of every group, and therefore its node id, is deterministic.
|
|
197
|
+
for (const key of [...byQueue.keys()].sort()) {
|
|
198
|
+
const ends = byQueue.get(key);
|
|
199
|
+
const producers = [...ends.producers].sort();
|
|
200
|
+
const consumers = [...ends.consumers].sort();
|
|
201
|
+
const api = graph.queues[key]?.api ?? key.split('.')[0];
|
|
202
|
+
const mergeKey = [api, producers.join(','), consumers.join(',')].join(PAIR_SEP);
|
|
203
|
+
const existing = groups.get(mergeKey);
|
|
204
|
+
if (existing === undefined)
|
|
205
|
+
groups.set(mergeKey, new QueueGroup([key], producers, consumers));
|
|
206
|
+
else
|
|
207
|
+
existing.members.push(key);
|
|
208
|
+
}
|
|
209
|
+
return [...groups.values()];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The queued half of the graph: producer → QUEUE → consumer, drawn with a sideways-cylinder node and
|
|
213
|
+
* DASHED arrows (an event returns as soon as it is queued).
|
|
214
|
+
*
|
|
215
|
+
* One BOX may list several queues, one per line. The unit Cloud Tasks and Terraform create is still
|
|
216
|
+
* the METHOD — that is what runtime-dependencies.json records and this render never changes it — but
|
|
217
|
+
* two methods of one contract flowing between exactly the same producers and consumers drew two
|
|
218
|
+
* adjacent boxes whose only difference was the method name, and each carried a `queue:` line that
|
|
219
|
+
* merely restated it. Listing them inside ONE box keeps every queue named (you can still see which
|
|
220
|
+
* one is stuck) at a fraction of the node count.
|
|
221
|
+
*/
|
|
222
|
+
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
223
|
+
function queuesDot(graph, hidden) {
|
|
224
|
+
const queued = graph.runtimeEdges.filter((e) => e.type === 'pubsub' && !hidden.has(e.from) && !hidden.has(e.to));
|
|
225
|
+
if (queued.length === 0)
|
|
226
|
+
return '';
|
|
227
|
+
let dot = '\n // Queued hops. Each LINE in a box is one Cloud Tasks queue; queues of one contract\n' +
|
|
228
|
+
' // sharing the same producers AND consumers are drawn in a single box.\n';
|
|
229
|
+
// A contract with no committed method table (a dependencies.json predating apiContracts) has no
|
|
230
|
+
// per-method queue at all, so it keeps the historical unnamed per-pair box.
|
|
231
|
+
const byQueue = new Map();
|
|
232
|
+
for (const edge of queued) {
|
|
233
|
+
const from = (0, dot_syntax_1.dotValue)(getShortName(edge.from));
|
|
234
|
+
const to = (0, dot_syntax_1.dotValue)(getShortName(edge.to));
|
|
235
|
+
if (edge.queue === undefined) {
|
|
236
|
+
// Kept RAW: recordValue already applies dotValue, so escaping here would double it.
|
|
237
|
+
const viaRaw = edge.via.map((v) => getShortName(v)).join(', ');
|
|
238
|
+
dot += queueBoxDot(`queue__${from}__${to}`, `${(0, dot_syntax_1.recordValue)(viaRaw)}\\nqueue`, [from], [to]);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!byQueue.has(edge.queue))
|
|
242
|
+
byQueue.set(edge.queue, new QueueEndpoints());
|
|
243
|
+
byQueue.get(edge.queue).producers.add(from);
|
|
244
|
+
byQueue.get(edge.queue).consumers.add(to);
|
|
245
|
+
}
|
|
246
|
+
for (const group of groupQueues(byQueue, graph)) {
|
|
247
|
+
const body = group.members
|
|
248
|
+
.map((key) => queueLine(key, graph.queues[key]))
|
|
249
|
+
.join('\\n');
|
|
250
|
+
dot += queueBoxDot(`queue__${dotId(group.members[0])}`, body, group.producers, group.consumers);
|
|
251
|
+
}
|
|
252
|
+
return dot;
|
|
200
253
|
}
|
|
201
254
|
/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */
|
|
202
255
|
// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file
|
|
203
256
|
function dotId(raw) {
|
|
204
257
|
return raw.replace(/[^A-Za-z0-9_]/g, '_');
|
|
205
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* ONE external-system node statement: the shape/fill of what the system IS, its label, and a
|
|
261
|
+
* parenthesised subtitle saying which way it faces.
|
|
262
|
+
*
|
|
263
|
+
* Shared by the OUTBOUND systems ({@link externalSystemsDot}) and the INBOUND callers
|
|
264
|
+
* ({@link triggerDot}) on purpose. They emit into the SAME `system__<identity>` id space, so a
|
|
265
|
+
* vendor this repo both calls and is called by is one box with arrows in both directions — and a
|
|
266
|
+
* copy-pasted attribute string would have let the two drift until they stopped being one box.
|
|
267
|
+
*/
|
|
268
|
+
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
269
|
+
function externalSystemNodeDot(identity, kind, label, subtitle) {
|
|
270
|
+
const shape = runtime_viz_theme_1.EXTERNAL_SHAPES[kind] ?? 'box';
|
|
271
|
+
const fill = runtime_viz_theme_1.EXTERNAL_FILLS[kind] ?? runtime_viz_theme_1.EXTERNAL_FILL;
|
|
272
|
+
// An Mrecord-shaped system needs the same empty leading field as a queue node, or it renders as
|
|
273
|
+
// a plain box and silently loses the sideways-cylinder read.
|
|
274
|
+
const isQueue = shape === runtime_viz_theme_1.QUEUE_SHAPE;
|
|
275
|
+
const prefix = isQueue ? runtime_viz_theme_1.QUEUE_LABEL_PREFIX : '';
|
|
276
|
+
const text = isQueue ? (0, dot_syntax_1.recordValue)(label) : (0, dot_syntax_1.dotValue)(label);
|
|
277
|
+
// Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be
|
|
278
|
+
// caught by the browser-side reshaping that lays queues on their side.
|
|
279
|
+
const marker = isQueue ? `class="${runtime_viz_theme_1.QUEUE_CLASS}", ` : '';
|
|
280
|
+
return (` "system__${dotId(identity)}" [shape=${shape}, style="filled", fillcolor="${fill}", ` +
|
|
281
|
+
`${marker}label="${prefix}${text}\\n(${subtitle})"];\n`);
|
|
282
|
+
}
|
|
206
283
|
/**
|
|
207
284
|
* The clock and outside-system nodes for endpoints NOTHING in-repo calls.
|
|
208
285
|
*
|
|
@@ -211,12 +288,26 @@ function dotId(raw) {
|
|
|
211
288
|
* absent — a server's most operationally interesting endpoint could be invisible on its own graph.
|
|
212
289
|
* Both are drawn pointing INTO the service that serves them, the opposite direction from
|
|
213
290
|
* {@link externalDot}'s outbound vendor calls.
|
|
291
|
+
*
|
|
292
|
+
* The inbound box names the CALLER (`twilio`), never the contract. Naming the contract was the whole
|
|
293
|
+
* bug: it restated what the service box directly below already says, while the one fact the box
|
|
294
|
+
* exists to convey — which vendor is posting to us — appeared nowhere. The contract keeps its place
|
|
295
|
+
* on the EDGE label, where `WhatsAppApi.inbound` reads as "…posts to this method".
|
|
296
|
+
*
|
|
297
|
+
* Identity is the caller's label in the SAME `system__` space {@link externalSystemsDot} uses, which
|
|
298
|
+
* buys three things at once: two vendors hitting one contract are two boxes, one vendor hitting three
|
|
299
|
+
* methods is one box with three arrows, and a vendor this repo also CALLS is that same single box.
|
|
300
|
+
* `options` is passed in for the last of those — when that outbound half will be drawn, this half
|
|
301
|
+
* must not restate the node statement.
|
|
214
302
|
*/
|
|
215
303
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
216
|
-
function triggerDot(graph, hidden) {
|
|
304
|
+
function triggerDot(graph, hidden, options) {
|
|
217
305
|
const triggers = graph.triggers.filter((t) => !hidden.has(t.service));
|
|
218
306
|
if (triggers.length === 0)
|
|
219
307
|
return '';
|
|
308
|
+
// Identities externalSystemsDot is about to draw; skipped here so the node is stated ONCE.
|
|
309
|
+
const drawnOutbound = new Set(options.showExternalNodes ? Object.keys(graph.externalSystems ?? {}) : []);
|
|
310
|
+
const emitted = new Set();
|
|
220
311
|
let dot = '\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\n';
|
|
221
312
|
for (const trigger of triggers) {
|
|
222
313
|
const service = (0, dot_syntax_1.dotValue)(getShortName(trigger.service));
|
|
@@ -225,16 +316,27 @@ function triggerDot(graph, hidden) {
|
|
|
225
316
|
const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;
|
|
226
317
|
const schedule = (0, dot_syntax_1.dotValue)(trigger.queueName ?? `${trigger.api}-${trigger.method}`);
|
|
227
318
|
dot +=
|
|
228
|
-
` "${id}" [shape=circle, style="filled", fillcolor="${CRON_FILL}", ` +
|
|
229
|
-
`color="${CRON_BORDER}", label="⏰\\ncron"];\n` +
|
|
230
|
-
` "${id}" -> "${service}" [label="${label}\\n${schedule}", color="${CRON_BORDER}"];\n`;
|
|
319
|
+
` "${id}" [shape=circle, style="filled", fillcolor="${runtime_viz_theme_1.CRON_FILL}", ` +
|
|
320
|
+
`color="${runtime_viz_theme_1.CRON_BORDER}", label="⏰\\ncron"];\n` +
|
|
321
|
+
` "${id}" -> "${service}" [label="${label}\\n${schedule}", color="${runtime_viz_theme_1.CRON_BORDER}"];\n`;
|
|
231
322
|
continue;
|
|
232
323
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
324
|
+
const caller = trigger.caller;
|
|
325
|
+
// Undeclared: only possible for a graph generated BEFORE the caller was required (generation
|
|
326
|
+
// now fails instead). DOTTED and question-marked, so "we were never told who this is" cannot
|
|
327
|
+
// be mistaken for a named vendor.
|
|
328
|
+
const id = caller === undefined
|
|
329
|
+
? `inbound__${dotId(trigger.api)}`
|
|
330
|
+
: `system__${dotId(caller.label)}`;
|
|
331
|
+
if (!emitted.has(id) && !(caller !== undefined && drawnOutbound.has(caller.label))) {
|
|
332
|
+
emitted.add(id);
|
|
333
|
+
dot +=
|
|
334
|
+
caller === undefined
|
|
335
|
+
? ` "${id}" [shape=box, style="dotted,filled", fillcolor="${runtime_viz_theme_1.EXTERNAL_FILL}", ` +
|
|
336
|
+
`color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${(0, dot_syntax_1.dotValue)(trigger.api)}\\n? unknown caller"];\n`
|
|
337
|
+
: externalSystemNodeDot(caller.label, caller.kind, caller.label, 'external caller');
|
|
338
|
+
}
|
|
339
|
+
dot += ` "${id}" -> "${service}" [label="${label}", style=dashed, color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n`;
|
|
238
340
|
}
|
|
239
341
|
return dot;
|
|
240
342
|
}
|
|
@@ -257,19 +359,7 @@ function externalSystemsDot(graph, hidden) {
|
|
|
257
359
|
let dot = '\n // Declared external systems — drawn with the shape of what they actually are.\n';
|
|
258
360
|
for (const id of ids) {
|
|
259
361
|
const system = systems[id];
|
|
260
|
-
|
|
261
|
-
const fill = EXTERNAL_FILLS[system.kind] ?? EXTERNAL_FILL;
|
|
262
|
-
// An Mrecord-shaped system needs the same empty leading field as a queue node, or it
|
|
263
|
-
// renders as a plain box and silently loses the sideways-cylinder read.
|
|
264
|
-
const isQueue = shape === QUEUE_SHAPE;
|
|
265
|
-
const prefix = isQueue ? QUEUE_LABEL_PREFIX : '';
|
|
266
|
-
const text = isQueue ? (0, dot_syntax_1.recordValue)(system.label) : (0, dot_syntax_1.dotValue)(system.label);
|
|
267
|
-
// Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be
|
|
268
|
-
// caught by the browser-side reshaping that lays queues on their side.
|
|
269
|
-
const marker = isQueue ? `class="${QUEUE_CLASS}", ` : '';
|
|
270
|
-
dot +=
|
|
271
|
-
` "system__${dotId(id)}" [shape=${shape}, style="filled", fillcolor="${fill}", ` +
|
|
272
|
-
`${marker}label="${prefix}${text}\\n(external ${(0, dot_syntax_1.dotValue)(system.kind)})"];\n`;
|
|
362
|
+
dot += externalSystemNodeDot(id, system.kind, system.label, `external ${(0, dot_syntax_1.dotValue)(system.kind)}`);
|
|
273
363
|
}
|
|
274
364
|
for (const id of ids) {
|
|
275
365
|
const system = systems[id];
|
|
@@ -317,8 +407,8 @@ function externalDot(graph, hidden) {
|
|
|
317
407
|
const externals = new Set([...apisByPair.keys()].map((key) => key.split(PAIR_SEP)[1]));
|
|
318
408
|
for (const external of [...externals].sort()) {
|
|
319
409
|
dot +=
|
|
320
|
-
` "external__${external}" [shape=box, style="dashed,filled", fillcolor="${EXTERNAL_FILL}", ` +
|
|
321
|
-
`color="${EXTERNAL_BORDER}", label="${external}\\n(external)"];\n`;
|
|
410
|
+
` "external__${external}" [shape=box, style="dashed,filled", fillcolor="${runtime_viz_theme_1.EXTERNAL_FILL}", ` +
|
|
411
|
+
`color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${external}\\n(external)"];\n`;
|
|
322
412
|
}
|
|
323
413
|
for (const key of [...apisByPair.keys()].sort()) {
|
|
324
414
|
const parts = key.split(PAIR_SEP);
|
|
@@ -329,7 +419,7 @@ function externalDot(graph, hidden) {
|
|
|
329
419
|
// "outside the repo" is already said by the node's dashed border.
|
|
330
420
|
dot +=
|
|
331
421
|
` "${(0, dot_syntax_1.dotValue)(getShortName(service))}" -> "external__${external}" ` +
|
|
332
|
-
`[label="${via}", color="${EXTERNAL_BORDER}"];\n`;
|
|
422
|
+
`[label="${via}", color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n`;
|
|
333
423
|
}
|
|
334
424
|
return dot;
|
|
335
425
|
}
|
|
@@ -347,16 +437,20 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
347
437
|
if (hidden.has(name))
|
|
348
438
|
continue;
|
|
349
439
|
const svc = graph.services[name];
|
|
350
|
-
const color = LEVEL_COLORS[svc.level] || '#F5F5F5';
|
|
440
|
+
const color = runtime_viz_theme_1.LEVEL_COLORS[svc.level] || '#F5F5F5';
|
|
351
441
|
dot += ` "${(0, dot_syntax_1.dotValue)(getShortName(name))}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`;
|
|
352
442
|
}
|
|
353
443
|
dot += '\n';
|
|
354
444
|
for (const edge of graph.runtimeEdges) {
|
|
355
445
|
if (hidden.has(edge.from) || hidden.has(edge.to))
|
|
356
446
|
continue;
|
|
357
|
-
|
|
447
|
+
// Queued hops are merged across methods, so they cannot be emitted one edge at a time.
|
|
448
|
+
if (edge.type === 'pubsub')
|
|
449
|
+
continue;
|
|
450
|
+
dot += edgeDot(edge);
|
|
358
451
|
}
|
|
359
|
-
dot +=
|
|
452
|
+
dot += queuesDot(graph, hidden);
|
|
453
|
+
dot += triggerDot(graph, hidden, options);
|
|
360
454
|
if (options.showExternalNodes) {
|
|
361
455
|
dot += externalSystemsDot(graph, hidden);
|
|
362
456
|
dot += externalDot(graph, hidden);
|
|
@@ -377,68 +471,6 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
377
471
|
* make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not
|
|
378
472
|
* otherwise have, since rendering happens in the browser.
|
|
379
473
|
*/
|
|
380
|
-
class LegendSwatches {
|
|
381
|
-
service = '<svg width="46" height="26"><rect x="1" y="3" width="44" height="20" rx="7" fill="#E8F5E9" stroke="#333"/></svg>';
|
|
382
|
-
/**
|
|
383
|
-
* A cylinder on its side — the SAME geometry runtime-visualizer.client.js draws on the real
|
|
384
|
-
* node, so the legend cannot drift from the picture it explains.
|
|
385
|
-
*/
|
|
386
|
-
queue = `<svg width="46" height="26"><path d="M9,5 H37 A8,8 0 0 1 37,21 H9 A8,8 0 0 1 9,5 Z" fill="${QUEUE_FILL}" stroke="#333"/>` +
|
|
387
|
-
'<path d="M9,5 A8,8 0 0 1 9,21" fill="none" stroke="#333"/></svg>';
|
|
388
|
-
database = `<svg width="46" height="26"><path d="M8,7 a15,4 0 0 1 30,0 v12 a15,4 0 0 1 -30,0 z" fill="${DATABASE_FILL}" stroke="#333"/>` +
|
|
389
|
-
'<path d="M8,7 a15,4 0 0 0 30,0" fill="none" stroke="#333"/></svg>';
|
|
390
|
-
storage = '<svg width="46" height="26"><path d="M2,22 V6 H16 l3,3 H44 V22 Z" fill="#F3E5F5" stroke="#333"/></svg>';
|
|
391
|
-
external = `<svg width="46" height="26"><rect x="1" y="3" width="44" height="20" fill="${EXTERNAL_FILL}" ` +
|
|
392
|
-
`stroke="${EXTERNAL_BORDER}" stroke-dasharray="4,3"/></svg>`;
|
|
393
|
-
cron = `<svg width="46" height="26"><circle cx="23" cy="13" r="11" fill="${CRON_FILL}" stroke="${CRON_BORDER}"/>` +
|
|
394
|
-
'<text x="23" y="18" font-size="12" text-anchor="middle">⏰</text></svg>';
|
|
395
|
-
solid = '<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="#333" stroke-width="1.5"/>' +
|
|
396
|
-
'<path d="M48,6 L57,10 L48,14 Z" fill="#333"/></svg>';
|
|
397
|
-
dashed = '<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="#333" stroke-width="1.5" ' +
|
|
398
|
-
'stroke-dasharray="5,4"/><path d="M48,6 L57,10 L48,14 Z" fill="#333"/></svg>';
|
|
399
|
-
scheduled = `<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="${CRON_BORDER}" stroke-width="1.5"/>` +
|
|
400
|
-
`<path d="M48,6 L57,10 L48,14 Z" fill="${CRON_BORDER}"/></svg>`;
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* The legend. Three columns — what a box IS, what a line MEANS, how to read a box — replacing the
|
|
404
|
-
* three paragraphs of prose that used to restate the picture in words. Styled after
|
|
405
|
-
* {@link GraphVisualizer}'s legend so the two graphs in this repo look like one tool.
|
|
406
|
-
*/
|
|
407
|
-
// webpieces-disable no-function-outside-class -- HTML builder, matching the sibling builders in this file
|
|
408
|
-
function legendHtml() {
|
|
409
|
-
const sw = new LegendSwatches();
|
|
410
|
-
const item = (swatch, text) => `<div class="legend-item"><span class="sw">${swatch}</span><span>${text}</span></div>`;
|
|
411
|
-
return `<div class="legend">
|
|
412
|
-
<h2>Legend</h2>
|
|
413
|
-
<div class="legend-columns">
|
|
414
|
-
<div class="legend-col">
|
|
415
|
-
<h3>Node shapes — <em>what a box is</em></h3>
|
|
416
|
-
${item(sw.service, '<strong>service</strong> — a deployable in this repo; fill is its dependency level')}
|
|
417
|
-
${item(sw.queue, '<strong>queue</strong> — one box <em>per method</em>, the unit Cloud Tasks and Terraform actually create')}
|
|
418
|
-
${item(sw.database, '<strong>database</strong> — a datastore outside this repo')}
|
|
419
|
-
${item(sw.storage, '<strong>object storage</strong> — a bucket outside this repo')}
|
|
420
|
-
${item(sw.external, '<strong>external system</strong> — outside this repo; nothing here implements it. Pointing <strong>OUT</strong> = a system this repo calls (firestore, gmail). Pointing <strong>IN</strong> = an endpoint driven from outside (a Pub/Sub push, a Gmail or Twilio webhook).')}
|
|
421
|
-
${item(sw.cron, '<strong>cron</strong> — a scheduler fires this endpoint')}
|
|
422
|
-
</div>
|
|
423
|
-
<div class="legend-col">
|
|
424
|
-
<h3>Lines — <em>what a call is</em></h3>
|
|
425
|
-
${item(sw.solid, '<strong>solid = rpc</strong> — the request follows the arrow, the response flows back')}
|
|
426
|
-
${item(sw.dashed, '<strong>dashed = event</strong> — asynchronous: the event flows in the direction of the arrow and returns once it is in the queue')}
|
|
427
|
-
${item(sw.scheduled, '<strong>scheduled</strong> — a cron invocation')}
|
|
428
|
-
<div class="legend-note"><em>Every line is labeled with the contract the call flows over. A service that enqueues to itself loops through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</em></div>
|
|
429
|
-
</div>
|
|
430
|
-
<div class="legend-col">
|
|
431
|
-
<h3>Reading a box</h3>
|
|
432
|
-
<pre class="legend-box-anatomy">name
|
|
433
|
-
(server|client, L#)
|
|
434
|
-
implements: <contracts it serves>
|
|
435
|
-
</pre>
|
|
436
|
-
<div class="legend-note">A box lists only what it <strong>serves</strong>. What it <em>calls</em> is on its outgoing arrows.</div>
|
|
437
|
-
<div class="legend-note"><code>(via <lib>)</code> = served through an embedded library, not its own source.</div>
|
|
438
|
-
</div>
|
|
439
|
-
</div>
|
|
440
|
-
</div>`;
|
|
441
|
-
}
|
|
442
474
|
function generateRuntimeHtml(dot, title) {
|
|
443
475
|
// The browser half lives in a plain .js asset (matching graph-visualizer.client.js) rather than
|
|
444
476
|
// in a template literal here: it renders with @viz-js/viz v3 AND redraws every queue node as a
|
|
@@ -494,7 +526,7 @@ function generateRuntimeHtml(dot, title) {
|
|
|
494
526
|
<body>
|
|
495
527
|
<h1>${title}</h1>
|
|
496
528
|
<div id="graph"></div>
|
|
497
|
-
${legendHtml()}
|
|
529
|
+
${(0, runtime_viz_theme_1.legendHtml)()}
|
|
498
530
|
<script>${script}</script>
|
|
499
531
|
</body>
|
|
500
532
|
</html>`;
|