@webpieces/nx-webpieces-rules 0.4.487 → 0.4.489
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/executors/generate/executor.js +42 -19
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-api-relations/executor.js +6 -1
- package/src/executors/validate-api-relations/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.js +65 -15
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-eslint-sync/executor.js +3 -2
- package/src/executors/validate-eslint-sync/executor.js.map +1 -1
- package/src/executors/validate-packagejson/executor.js +4 -2
- package/src/executors/validate-packagejson/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +3 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/executors/validate-versions-locked/executor.js +3 -2
- package/src/executors/validate-versions-locked/executor.js.map +1 -1
- package/src/lib/api-usage/api-ast.d.ts +71 -0
- package/src/lib/api-usage/api-ast.js +250 -0
- package/src/lib/api-usage/api-ast.js.map +1 -0
- package/src/lib/api-usage/api-relations.d.ts +72 -3
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +43 -4
- package/src/lib/api-usage/api-scanner.js +121 -103
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/graph-loader.d.ts +21 -2
- package/src/lib/graph-loader.js +38 -4
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/rule-gate.d.ts +8 -5
- package/src/lib/rule-gate.js +8 -5
- package/src/lib/rule-gate.js.map +1 -1
- package/src/lib/runtime-config.d.ts +7 -1
- package/src/lib/runtime-config.js +7 -1
- package/src/lib/runtime-config.js.map +1 -1
- package/src/lib/runtime-graph-io.d.ts +17 -0
- package/src/lib/runtime-graph-io.js +59 -0
- package/src/lib/runtime-graph-io.js.map +1 -0
- package/src/lib/runtime-graph-model.d.ts +121 -0
- package/src/lib/runtime-graph-model.js +14 -0
- package/src/lib/runtime-graph-model.js.map +1 -0
- package/src/lib/runtime-graph.d.ts +6 -75
- package/src/lib/runtime-graph.js +163 -51
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +5 -0
- package/src/lib/runtime-visualizer.js +63 -6
- package/src/lib/runtime-visualizer.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type { ApiTransport } from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n}\n"]}
|
|
@@ -28,74 +28,10 @@
|
|
|
28
28
|
* one, so it must never degrade silently.
|
|
29
29
|
*/
|
|
30
30
|
import type { EnhancedGraph } from './graph-sorter';
|
|
31
|
-
import type {
|
|
32
|
-
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its
|
|
37
|
-
* project.json. Absent for a service nothing calls by name (e.g. a browser app).
|
|
38
|
-
*/
|
|
39
|
-
serviceName?: string;
|
|
40
|
-
/**
|
|
41
|
-
* The service(s) this node's clients call when the call site carries no literal `ClientConfig`,
|
|
42
|
-
* declared in its project.json (metadata.webpieces.callsService). A single name, or an
|
|
43
|
-
* `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors
|
|
44
|
-
* GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.
|
|
45
|
-
*/
|
|
46
|
-
callsService?: string | Record<string, string>;
|
|
47
|
-
implements: string[];
|
|
48
|
-
/**
|
|
49
|
-
* apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis
|
|
50
|
-
* this service serves through an embedded library rather than its own source (e.g. a shared
|
|
51
|
-
* route-registration lib). Answers "who implements WarmupApi, and where did that come from?",
|
|
52
|
-
* which previously required walking the dependsOn closure by hand.
|
|
53
|
-
*/
|
|
54
|
-
implementsVia?: Record<string, string>;
|
|
55
|
-
uses: string[];
|
|
56
|
-
dependsOn: string[];
|
|
57
|
-
/**
|
|
58
|
-
* When false, this service is hidden from the rendered runtime graph (its
|
|
59
|
-
* node AND every edge touching it are omitted from the HTML/DOT). It stays
|
|
60
|
-
* in runtime-dependencies.json so the data view is complete. Absent means
|
|
61
|
-
* drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`
|
|
62
|
-
* nx tag.
|
|
63
|
-
*/
|
|
64
|
-
drawOnGraph?: boolean;
|
|
65
|
-
}
|
|
66
|
-
export interface RuntimeApi {
|
|
67
|
-
implementedBy: string[];
|
|
68
|
-
usedBy: string[];
|
|
69
|
-
/** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */
|
|
70
|
-
type?: ApiTransport;
|
|
71
|
-
/**
|
|
72
|
-
* The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this
|
|
73
|
-
* is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which
|
|
74
|
-
* is what the runtime viz labels its terminal external nodes with.
|
|
75
|
-
*/
|
|
76
|
-
owner?: string;
|
|
77
|
-
}
|
|
78
|
-
export interface RuntimeEdge {
|
|
79
|
-
from: string;
|
|
80
|
-
to: string;
|
|
81
|
-
via: string[];
|
|
82
|
-
/**
|
|
83
|
-
* Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the
|
|
84
|
-
* consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.
|
|
85
|
-
* Edges are split by transport, so every edge is a single kind.
|
|
86
|
-
*/
|
|
87
|
-
type?: ApiTransport;
|
|
88
|
-
}
|
|
89
|
-
export interface RuntimeUnresolved {
|
|
90
|
-
service: string;
|
|
91
|
-
api: string;
|
|
92
|
-
}
|
|
93
|
-
export interface RuntimeGraph {
|
|
94
|
-
services: Record<string, RuntimeService>;
|
|
95
|
-
apis: Record<string, RuntimeApi>;
|
|
96
|
-
runtimeEdges: RuntimeEdge[];
|
|
97
|
-
unresolvedUses: RuntimeUnresolved[];
|
|
98
|
-
}
|
|
31
|
+
import type { ApiContracts } from './api-usage/api-relations';
|
|
32
|
+
import type { RuntimeGraph } from './runtime-graph-model';
|
|
33
|
+
export type { RuntimeApi, RuntimeEdge, RuntimeGraph, RuntimeQueue, RuntimeService, RuntimeTrigger, RuntimeUnresolved, } from './runtime-graph-model';
|
|
34
|
+
export { DEFAULT_RUNTIME_GRAPH_PATH, saveRuntimeGraph, runtimeGraphFileExists, loadRuntimeGraph, serializeRuntimeGraph, } from './runtime-graph-io';
|
|
99
35
|
/**
|
|
100
36
|
* The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
|
|
101
37
|
* part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
|
|
@@ -133,16 +69,11 @@ export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, st
|
|
|
133
69
|
* (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
|
|
134
70
|
* runtime visualizer omits their nodes + edges.
|
|
135
71
|
*/
|
|
136
|
-
export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string
|
|
72
|
+
export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>, apiContracts?: ApiContracts): RuntimeGraph;
|
|
137
73
|
/**
|
|
138
74
|
* The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
|
|
139
75
|
* this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that
|
|
140
76
|
* only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph
|
|
141
77
|
* file that records its own doubts would just get committed and stop being read.
|
|
142
78
|
*/
|
|
143
|
-
export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string
|
|
144
|
-
export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
|
|
145
|
-
export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
|
|
146
|
-
export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
|
|
147
|
-
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
148
|
-
export declare function serializeRuntimeGraph(graph: RuntimeGraph): string;
|
|
79
|
+
export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string>, apiContracts?: ApiContracts): RuntimeGraphReport;
|
package/src/lib/runtime-graph.js
CHANGED
|
@@ -29,21 +29,20 @@
|
|
|
29
29
|
* one, so it must never degrade silently.
|
|
30
30
|
*/
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.RuntimeGraphReport = exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
32
|
+
exports.RuntimeGraphReport = exports.serializeRuntimeGraph = exports.loadRuntimeGraph = exports.runtimeGraphFileExists = exports.saveRuntimeGraph = exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
33
33
|
exports.runtimeAdjacency = runtimeAdjacency;
|
|
34
34
|
exports.deriveRuntimeGraph = deriveRuntimeGraph;
|
|
35
35
|
exports.deriveRuntimeGraphReport = deriveRuntimeGraphReport;
|
|
36
|
-
exports.saveRuntimeGraph = saveRuntimeGraph;
|
|
37
|
-
exports.runtimeGraphFileExists = runtimeGraphFileExists;
|
|
38
|
-
exports.loadRuntimeGraph = loadRuntimeGraph;
|
|
39
|
-
exports.serializeRuntimeGraph = serializeRuntimeGraph;
|
|
40
|
-
const tslib_1 = require("tslib");
|
|
41
|
-
const fs = tslib_1.__importStar(require("fs"));
|
|
42
|
-
const path = tslib_1.__importStar(require("path"));
|
|
43
36
|
const graph_sorter_1 = require("./graph-sorter");
|
|
44
37
|
const api_relations_1 = require("./api-usage/api-relations");
|
|
45
38
|
const toError_1 = require("../toError");
|
|
46
|
-
|
|
39
|
+
// Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.
|
|
40
|
+
var runtime_graph_io_1 = require("./runtime-graph-io");
|
|
41
|
+
Object.defineProperty(exports, "DEFAULT_RUNTIME_GRAPH_PATH", { enumerable: true, get: function () { return runtime_graph_io_1.DEFAULT_RUNTIME_GRAPH_PATH; } });
|
|
42
|
+
Object.defineProperty(exports, "saveRuntimeGraph", { enumerable: true, get: function () { return runtime_graph_io_1.saveRuntimeGraph; } });
|
|
43
|
+
Object.defineProperty(exports, "runtimeGraphFileExists", { enumerable: true, get: function () { return runtime_graph_io_1.runtimeGraphFileExists; } });
|
|
44
|
+
Object.defineProperty(exports, "loadRuntimeGraph", { enumerable: true, get: function () { return runtime_graph_io_1.loadRuntimeGraph; } });
|
|
45
|
+
Object.defineProperty(exports, "serializeRuntimeGraph", { enumerable: true, get: function () { return runtime_graph_io_1.serializeRuntimeGraph; } });
|
|
47
46
|
/**
|
|
48
47
|
* The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
|
|
49
48
|
* part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
|
|
@@ -70,12 +69,22 @@ class RuntimeGraphReport {
|
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
exports.RuntimeGraphReport = RuntimeGraphReport;
|
|
73
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Adjacency (service -> [targets]) used for leveling + cycle checks.
|
|
74
|
+
*
|
|
75
|
+
* PUBSUB EDGES ARE EXCLUDED. A queue is precisely the thing that decouples producer from consumer:
|
|
76
|
+
* the producer returns as soon as the task is enqueued and never waits on the consumer, so a queued
|
|
77
|
+
* hop is not a runtime dependency in the sense levels and cycle detection mean. Counting them would
|
|
78
|
+
* make the common and correct `A → queue → A` (a service deferring its own work) an architecture
|
|
79
|
+
* cycle, and would rank services by an ordering that does not constrain deploy or startup.
|
|
80
|
+
*/
|
|
74
81
|
function adjacencyFromEdges(serviceNames, edges) {
|
|
75
82
|
const adj = {};
|
|
76
83
|
for (const name of serviceNames)
|
|
77
84
|
adj[name] = [];
|
|
78
85
|
for (const edge of edges) {
|
|
86
|
+
if (edge.type === 'pubsub')
|
|
87
|
+
continue;
|
|
79
88
|
if (!adj[edge.from])
|
|
80
89
|
adj[edge.from] = [];
|
|
81
90
|
adj[edge.from].push(edge.to);
|
|
@@ -141,6 +150,7 @@ class RelationSink {
|
|
|
141
150
|
class RuntimeGraphDeriver {
|
|
142
151
|
projects;
|
|
143
152
|
hiddenProjects;
|
|
153
|
+
apiContracts;
|
|
144
154
|
/** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */
|
|
145
155
|
apiOwners = new Map();
|
|
146
156
|
/** Addressable name -> the runtime node answering to it; how a targeted call resolves. */
|
|
@@ -149,9 +159,16 @@ class RuntimeGraphDeriver {
|
|
|
149
159
|
problems = [];
|
|
150
160
|
constructor(projects,
|
|
151
161
|
/** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */
|
|
152
|
-
hiddenProjects
|
|
162
|
+
hiddenProjects,
|
|
163
|
+
/**
|
|
164
|
+
* The committed per-contract method table from dependencies.json. Empty means the file
|
|
165
|
+
* predates `apiContracts`: every pubsub edge then falls back to one unnamed queue per
|
|
166
|
+
* service pair, exactly as before, instead of failing on a missing table.
|
|
167
|
+
*/
|
|
168
|
+
apiContracts = {}) {
|
|
153
169
|
this.projects = projects;
|
|
154
170
|
this.hiddenProjects = hiddenProjects;
|
|
171
|
+
this.apiContracts = apiContracts;
|
|
155
172
|
// A node ALWAYS answers to its own module name, so a repo whose deployed names match its
|
|
156
173
|
// project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away
|
|
157
174
|
// from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.
|
|
@@ -189,9 +206,46 @@ class RuntimeGraphDeriver {
|
|
|
189
206
|
apis: apisObj,
|
|
190
207
|
runtimeEdges: edgeResult.edges,
|
|
191
208
|
unresolvedUses: edgeResult.unresolved,
|
|
209
|
+
queues: edgeResult.queues,
|
|
210
|
+
triggers: this.buildTriggers(decls),
|
|
192
211
|
};
|
|
193
212
|
return new RuntimeGraphReport(graph, this.warnings, this.problems);
|
|
194
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method
|
|
216
|
+
* declared `cron` or `external` becomes a trigger pointing AT that node.
|
|
217
|
+
*
|
|
218
|
+
* Driven off `implements` rather than `uses` on purpose — these have no in-repo caller at all,
|
|
219
|
+
* which is why they never produced an edge and stayed invisible.
|
|
220
|
+
*/
|
|
221
|
+
buildTriggers(decls) {
|
|
222
|
+
const triggers = [];
|
|
223
|
+
for (const decl of decls) {
|
|
224
|
+
for (const ref of decl.implementsApis) {
|
|
225
|
+
for (const method of this.methodsOf(ref.api)) {
|
|
226
|
+
if (method.kind !== 'cron' && method.kind !== 'external')
|
|
227
|
+
continue;
|
|
228
|
+
const trigger = {
|
|
229
|
+
kind: method.kind,
|
|
230
|
+
api: ref.api,
|
|
231
|
+
method: method.name,
|
|
232
|
+
service: decl.name,
|
|
233
|
+
};
|
|
234
|
+
if (method.kind === 'cron')
|
|
235
|
+
trigger.queueName = method.queueName;
|
|
236
|
+
triggers.push(trigger);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return triggers.sort((a, b) => a.kind.localeCompare(b.kind) ||
|
|
241
|
+
a.api.localeCompare(b.api) ||
|
|
242
|
+
a.method.localeCompare(b.method) ||
|
|
243
|
+
a.service.localeCompare(b.service));
|
|
244
|
+
}
|
|
245
|
+
/** The committed method table for a contract, or [] when it declares none (e.g. a vendor seam). */
|
|
246
|
+
methodsOf(api) {
|
|
247
|
+
return this.apiContracts[api]?.methods ?? [];
|
|
248
|
+
}
|
|
195
249
|
/**
|
|
196
250
|
* One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective
|
|
197
251
|
* relations are its OWN apiRelations PLUS those of every library in its transitive
|
|
@@ -287,16 +341,26 @@ class RuntimeGraphDeriver {
|
|
|
287
341
|
buildEdges(decls, apis) {
|
|
288
342
|
const viaByKey = new Map();
|
|
289
343
|
const unresolved = [];
|
|
344
|
+
const queues = new Map();
|
|
290
345
|
for (const decl of decls) {
|
|
291
346
|
for (const ref of decl.usesApis) {
|
|
292
347
|
const implementers = apis.get(ref.api)?.implementedBy ?? [];
|
|
293
348
|
if (implementers.length === 0) {
|
|
349
|
+
// Nobody in-repo serves it. For a vendor contract that is the ANSWER, not a gap:
|
|
350
|
+
// the call leaves the repo, and the viz terminates it at a dashed vendor node.
|
|
294
351
|
unresolved.push({ service: decl.name, api: ref.api });
|
|
295
352
|
continue;
|
|
296
353
|
}
|
|
297
354
|
for (const target of this.targetsFor(decl.name, ref, implementers)) {
|
|
298
|
-
|
|
355
|
+
// A service calling ITSELF synchronously is noise; a service ENQUEUEING to
|
|
356
|
+
// itself is a real, common topology (deferring its own work), and dropping it
|
|
357
|
+
// was hiding the single most interesting thing about a queue.
|
|
358
|
+
if (target === decl.name && ref.type !== 'pubsub')
|
|
359
|
+
continue;
|
|
360
|
+
if (ref.type === 'pubsub') {
|
|
361
|
+
this.addQueuedEdges(decl.name, target, ref.api, viaByKey, queues);
|
|
299
362
|
continue;
|
|
363
|
+
}
|
|
300
364
|
const key = `${decl.name} ${target} ${ref.type}`;
|
|
301
365
|
if (!viaByKey.has(key))
|
|
302
366
|
viaByKey.set(key, new Set());
|
|
@@ -304,7 +368,53 @@ class RuntimeGraphDeriver {
|
|
|
304
368
|
}
|
|
305
369
|
}
|
|
306
370
|
}
|
|
307
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
edges: this.edgesFromKeys(viaByKey),
|
|
373
|
+
unresolved: sortUnresolved(unresolved),
|
|
374
|
+
queues: sortedQueues(queues),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* One edge PER QUEUED METHOD of `api`, plus the queue each flows through.
|
|
379
|
+
*
|
|
380
|
+
* Per method rather than per service pair because that is the unit Cloud Tasks and Terraform
|
|
381
|
+
* create: two services exchanging three queued methods run three independently-configured,
|
|
382
|
+
* independently-backed-up queues, and collapsing them into one arrow hides which one is stuck.
|
|
383
|
+
*
|
|
384
|
+
* A contract with no committed method table (a dependencies.json predating `apiContracts`)
|
|
385
|
+
* degrades to a single unnamed queue for the pair — the old behavior — rather than vanishing.
|
|
386
|
+
*/
|
|
387
|
+
addQueuedEdges(from, to, api, viaByKey, queues) {
|
|
388
|
+
const queued = this.methodsOf(api).filter((m) => m.kind === 'cloudtasks');
|
|
389
|
+
if (queued.length === 0) {
|
|
390
|
+
const key = `${from} ${to} pubsub `;
|
|
391
|
+
if (!viaByKey.has(key))
|
|
392
|
+
viaByKey.set(key, new Set());
|
|
393
|
+
viaByKey.get(key).add(api);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
for (const method of queued) {
|
|
397
|
+
const queueKey = `${api}.${method.name}`;
|
|
398
|
+
const key = `${from} ${to} pubsub ${queueKey}`;
|
|
399
|
+
if (!viaByKey.has(key))
|
|
400
|
+
viaByKey.set(key, new Set());
|
|
401
|
+
viaByKey.get(key).add(api);
|
|
402
|
+
let queue = queues.get(queueKey);
|
|
403
|
+
if (queue === undefined) {
|
|
404
|
+
queue = {
|
|
405
|
+
api,
|
|
406
|
+
method: method.name,
|
|
407
|
+
queueName: method.queueName,
|
|
408
|
+
producedBy: [],
|
|
409
|
+
consumedBy: [],
|
|
410
|
+
};
|
|
411
|
+
queues.set(queueKey, queue);
|
|
412
|
+
}
|
|
413
|
+
if (!queue.producedBy.includes(from))
|
|
414
|
+
queue.producedBy.push(from);
|
|
415
|
+
if (!queue.consumedBy.includes(to))
|
|
416
|
+
queue.consumedBy.push(to);
|
|
417
|
+
}
|
|
308
418
|
}
|
|
309
419
|
/**
|
|
310
420
|
* WHICH implementers this one `uses` reaches. Resolution order (most specific wins):
|
|
@@ -381,19 +491,40 @@ class RuntimeGraphDeriver {
|
|
|
381
491
|
}
|
|
382
492
|
return implementers;
|
|
383
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Rebuild edges from their `"from to type queue"` keys. The 4th part is the queue ("Api.method")
|
|
496
|
+
* and is empty for every non-queued edge; it is part of the KEY so two queued methods between
|
|
497
|
+
* the same pair stay two edges instead of collapsing into one.
|
|
498
|
+
*/
|
|
384
499
|
edgesFromKeys(viaByKey) {
|
|
385
500
|
const edges = [];
|
|
386
501
|
for (const key of viaByKey.keys()) {
|
|
387
502
|
const parts = key.split(' ');
|
|
388
|
-
|
|
503
|
+
const edge = {
|
|
504
|
+
from: parts[0],
|
|
505
|
+
to: parts[1],
|
|
506
|
+
via: Array.from(viaByKey.get(key)).sort(),
|
|
507
|
+
type: parts[2],
|
|
508
|
+
};
|
|
509
|
+
if (parts[3] !== undefined && parts[3] !== '')
|
|
510
|
+
edge.queue = parts[3];
|
|
511
|
+
edges.push(edge);
|
|
389
512
|
}
|
|
390
|
-
edges.sort((a, b) => a.from.localeCompare(b.from) ||
|
|
513
|
+
edges.sort((a, b) => a.from.localeCompare(b.from) ||
|
|
514
|
+
a.to.localeCompare(b.to) ||
|
|
515
|
+
(a.type ?? '').localeCompare(b.type ?? '') ||
|
|
516
|
+
(a.queue ?? '').localeCompare(b.queue ?? ''));
|
|
391
517
|
return edges;
|
|
392
518
|
}
|
|
393
519
|
buildServices(decls, edges) {
|
|
394
520
|
const services = {};
|
|
395
521
|
for (const decl of decls) {
|
|
396
|
-
|
|
522
|
+
// A queued hop depends on the QUEUE, not on the peer: the producer hands off and returns,
|
|
523
|
+
// so naming the consumer here would assert a coupling that does not exist (and would make
|
|
524
|
+
// a service that defers work to itself look self-dependent).
|
|
525
|
+
const dependsOn = Array.from(new Set(edges
|
|
526
|
+
.filter((e) => e.from === decl.name)
|
|
527
|
+
.map((e) => (e.queue === undefined ? e.to : `queue:${e.queue}`)))).sort();
|
|
397
528
|
// Keys are written in this order; an undefined value is omitted by JSON.stringify, so
|
|
398
529
|
// the committed JSON stays clean AND deterministic without conditional assembly.
|
|
399
530
|
const service = {
|
|
@@ -423,8 +554,8 @@ class RuntimeGraphDeriver {
|
|
|
423
554
|
* runtime visualizer omits their nodes + edges.
|
|
424
555
|
*/
|
|
425
556
|
// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
|
|
426
|
-
function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
|
|
427
|
-
return deriveRuntimeGraphReport(projects, hiddenProjects).graph;
|
|
557
|
+
function deriveRuntimeGraph(projects, hiddenProjects = new Set(), apiContracts = {}) {
|
|
558
|
+
return deriveRuntimeGraphReport(projects, hiddenProjects, apiContracts).graph;
|
|
428
559
|
}
|
|
429
560
|
/**
|
|
430
561
|
* The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
|
|
@@ -433,8 +564,8 @@ function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
|
|
|
433
564
|
* file that records its own doubts would just get committed and stop being read.
|
|
434
565
|
*/
|
|
435
566
|
// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
|
|
436
|
-
function deriveRuntimeGraphReport(projects, hiddenProjects = new Set()) {
|
|
437
|
-
return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();
|
|
567
|
+
function deriveRuntimeGraphReport(projects, hiddenProjects = new Set(), apiContracts = {}) {
|
|
568
|
+
return new RuntimeGraphDeriver(projects, hiddenProjects, apiContracts).assemble();
|
|
438
569
|
}
|
|
439
570
|
/** Drop duplicate api refs, keeping the first — needed after a node absorbs the same api from both
|
|
440
571
|
* its own relations and an embedded lib's. Keyed by api AND target service: the same contract aimed
|
|
@@ -453,6 +584,18 @@ function dedupApiRefs(refs) {
|
|
|
453
584
|
}
|
|
454
585
|
return out;
|
|
455
586
|
}
|
|
587
|
+
/** Queues as a key-sorted object, with each producer/consumer list sorted, for a deterministic file. */
|
|
588
|
+
// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file
|
|
589
|
+
function sortedQueues(queues) {
|
|
590
|
+
const out = {};
|
|
591
|
+
for (const key of [...queues.keys()].sort()) {
|
|
592
|
+
const queue = queues.get(key);
|
|
593
|
+
queue.producedBy.sort();
|
|
594
|
+
queue.consumedBy.sort();
|
|
595
|
+
out[key] = queue;
|
|
596
|
+
}
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
456
599
|
/** Sort a Map into a plain object with sorted keys, so the committed JSON is deterministic. */
|
|
457
600
|
// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file
|
|
458
601
|
function sortedRecord(map) {
|
|
@@ -469,35 +612,4 @@ function sortUnresolved(unresolved) {
|
|
|
469
612
|
byKey.set(`${entry.service} ${entry.api}`, entry);
|
|
470
613
|
return [...byKey.values()].sort((a, b) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api));
|
|
471
614
|
}
|
|
472
|
-
/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */
|
|
473
|
-
function formatRuntimeJson(graph) {
|
|
474
|
-
return JSON.stringify(graph, null, 4) + '\n';
|
|
475
|
-
}
|
|
476
|
-
function saveRuntimeGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
477
|
-
const fullPath = path.join(workspaceRoot, graphPath);
|
|
478
|
-
const dir = path.dirname(fullPath);
|
|
479
|
-
if (!fs.existsSync(dir))
|
|
480
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
481
|
-
fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');
|
|
482
|
-
}
|
|
483
|
-
function runtimeGraphFileExists(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
484
|
-
return fs.existsSync(path.join(workspaceRoot, graphPath));
|
|
485
|
-
}
|
|
486
|
-
function loadRuntimeGraph(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
487
|
-
const fullPath = path.join(workspaceRoot, graphPath);
|
|
488
|
-
if (!fs.existsSync(fullPath))
|
|
489
|
-
return null;
|
|
490
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
491
|
-
try {
|
|
492
|
-
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
493
|
-
}
|
|
494
|
-
catch (err) {
|
|
495
|
-
const error = (0, toError_1.toError)(err);
|
|
496
|
-
throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
500
|
-
function serializeRuntimeGraph(graph) {
|
|
501
|
-
return formatRuntimeJson(graph);
|
|
502
|
-
}
|
|
503
615
|
//# sourceMappingURL=runtime-graph.js.map
|