@eventcatalog/core 5.7.2 → 5.7.5
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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-OR4VBY3F.js → chunk-3I4MO4G6.js} +1 -1
- package/dist/{chunk-B3FUBDJN.js → chunk-NNR5RP47.js} +1 -1
- package/dist/{chunk-EPI5BHEU.js → chunk-Q6ZCTZM3.js} +1 -1
- package/dist/{chunk-TYZENGGJ.js → chunk-SAB3WVWU.js} +1 -1
- package/dist/{chunk-NNTWQNQ5.js → chunk-SR6NZ4QL.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/01-what-is-eventcatalog-assistant.md +8 -0
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +24 -0
- package/dist/docs/development/guides/domains/06-visualize-domains/01-domain-diagram.md +2 -1
- package/dist/docs/development/studio/01-introduction.md +2 -1
- package/dist/docs/development/studio/04-collaborate-on-a-canvas.md +10 -2
- package/dist/docs/development/studio/05-connect-your-agent.md +4 -0
- package/dist/docs/development/studio/07-reference.md +40 -7
- package/dist/docs/development/studio/draw-with-ai.md +96 -0
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/Steps/Step.astro +4 -2
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +2 -103
- package/eventcatalog/src/components/SchemaExplorer/diff-dark-styles.ts +104 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +68 -68
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_ChangelogPage.astro +297 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.spec.ts +101 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +76 -47
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +4 -168
- package/eventcatalog/src/pages/docs/services/[id]/[version]/changelog/index.astro +14 -0
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +3 -3
- package/eventcatalog/src/utils/node-graphs/channel-node-graph.ts +2 -2
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +248 -71
- package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +0 -87
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import { getNodesAndEdges as getServiceNodesAndEdges } from './services-node-gra
|
|
|
4
4
|
import { getNodesAndEdgesForDomainSystems as getDomainContextNodesAndEdges } from './system-context-node-graph';
|
|
5
5
|
import { getServices } from '@utils/collections/services';
|
|
6
6
|
import { getAgents } from '@utils/collections/agents';
|
|
7
|
+
import { versionMatches } from '@utils/collections/util';
|
|
7
8
|
import { createTriggerMessageNode } from './message-node-graph';
|
|
8
9
|
import {
|
|
9
10
|
createEdge,
|
|
@@ -28,6 +29,8 @@ type Tree = { domain: any; groupId: string; parentId?: string }[];
|
|
|
28
29
|
type Owners = Map<string, { domain: any; system?: any }>;
|
|
29
30
|
|
|
30
31
|
const MESSAGE_TYPES = new Set(['events', 'commands', 'queries']);
|
|
32
|
+
// Commands and queries belong to what receives them (it handles them), as events do to what sends them
|
|
33
|
+
const RECEIVER_OWNED_TYPES = new Set(['commands', 'queries']);
|
|
31
34
|
const SERVICE_TYPES = new Set(['services', 'agents']);
|
|
32
35
|
|
|
33
36
|
const systemGroupId = (system: any) => `system-group-${system.data.id}-${system.data.version}`;
|
|
@@ -43,6 +46,45 @@ const isInside = (nodes: Map<string, any>, nodeId: string, groupId: string) => {
|
|
|
43
46
|
return false;
|
|
44
47
|
};
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether a node (or the services another domain stands for, `receivedBy`) says
|
|
51
|
+
* it receives a message: its id, and its version (or range, or latest). Undefined
|
|
52
|
+
* when it doesn't say what it receives
|
|
53
|
+
*/
|
|
54
|
+
const saysItReceives = (node: any, message: any, receivedBy?: any[]): boolean | undefined => {
|
|
55
|
+
const receives = (node?.data?.service ?? node?.data?.agent)?.receives ?? receivedBy;
|
|
56
|
+
if (!receives) return undefined;
|
|
57
|
+
const { id, version } = message?.data?.message ?? {};
|
|
58
|
+
return receives.some((pointer: any) => {
|
|
59
|
+
const received = pointer.data ?? pointer;
|
|
60
|
+
return received.id === id && versionMatches(version ?? '', received.version ?? 'latest');
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What a message reaches, through any channels (not the channels themselves).
|
|
66
|
+
* Past a channel, only what doesn't say it receives something else: a channel
|
|
67
|
+
* can carry other messages, to consumers of their own
|
|
68
|
+
*/
|
|
69
|
+
const receiversThroughChannels = (messageId: string, nodes: Map<string, any>, receiversOf: Map<string, string[]>) => {
|
|
70
|
+
const message = nodes.get(messageId);
|
|
71
|
+
const receivers: string[] = [];
|
|
72
|
+
const seen = new Set([messageId]);
|
|
73
|
+
for (let current = [messageId]; current.length > 0; ) {
|
|
74
|
+
const throughChannel = current[0] !== messageId;
|
|
75
|
+
const next = current.flatMap((nodeId) => receiversOf.get(nodeId) ?? []).filter((nodeId) => !seen.has(nodeId));
|
|
76
|
+
next.forEach((nodeId) => seen.add(nodeId));
|
|
77
|
+
receivers.push(
|
|
78
|
+
...next.filter(
|
|
79
|
+
(nodeId) =>
|
|
80
|
+
nodes.get(nodeId)?.type !== 'channels' && (!throughChannel || saysItReceives(nodes.get(nodeId), message) !== false)
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
current = next.filter((nodeId) => nodes.get(nodeId)?.type === 'channels');
|
|
84
|
+
}
|
|
85
|
+
return receivers;
|
|
86
|
+
};
|
|
87
|
+
|
|
46
88
|
/** A system shown as a single node (not expanded into what's inside it) */
|
|
47
89
|
const createSystemNode = (system: any, parentId?: string) => ({
|
|
48
90
|
id: generateIdForNode(system),
|
|
@@ -97,14 +139,16 @@ const createDomainCard = (domain: any, { parentId, subdomain = false }: { parent
|
|
|
97
139
|
* systems is shown as a card. Messages a (sub)domain sends or receives itself
|
|
98
140
|
* (`domainMessages`: by message, the boxes drawn with it) fold into it, as a
|
|
99
141
|
* system's do into the system, when only one does (more than one, they connect
|
|
100
|
-
* through the message instead, rather than to each other).
|
|
142
|
+
* through the message instead, rather than to each other). `receivedBy` has
|
|
143
|
+
* what the services other domains stand for receive, by the node shown for them.
|
|
101
144
|
*/
|
|
102
145
|
export const collapseSystems = (
|
|
103
146
|
graph: Graph,
|
|
104
147
|
systems: any[],
|
|
105
148
|
contextNodes: Map<string, any>,
|
|
106
149
|
subdomains: Map<string, any>,
|
|
107
|
-
domainMessages: Map<string, Set<string>> = new Map()
|
|
150
|
+
domainMessages: Map<string, Set<string>> = new Map(),
|
|
151
|
+
receivedBy: Map<string, any[]> = new Map()
|
|
108
152
|
): Graph => {
|
|
109
153
|
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
110
154
|
const systemsByGroupId = new Map<string, any>(systems.map((system: any) => [systemGroupId(system), system]));
|
|
@@ -123,11 +167,32 @@ export const collapseSystems = (
|
|
|
123
167
|
cards.set(groupId, createDomainCard(subdomain, { parentId: nodesById.get(groupId)?.parentId, subdomain: true }));
|
|
124
168
|
});
|
|
125
169
|
|
|
170
|
+
const sendersOf = new Map<string, string[]>();
|
|
171
|
+
graph.edges.forEach((edge) => sendersOf.set(edge.target, [...(sendersOf.get(edge.target) ?? []), edge.source]));
|
|
172
|
+
const carries = (nodeId: string) => nodesById.has(nodeId) && isCarrier(nodesById.get(nodeId));
|
|
173
|
+
const isMessage = (nodeId: string) => MESSAGE_TYPES.has(nodesById.get(nodeId)?.type);
|
|
174
|
+
const isChannel = (nodeId: string) => nodesById.get(nodeId)?.type === 'channels';
|
|
175
|
+
|
|
176
|
+
// A message is shown as what sends it, not where it's drawn, which can be with
|
|
177
|
+
// one of its consumers. Sent from more than one, it's left as it is (connecting
|
|
178
|
+
// each sender to what receives it, rather than one to the rest). None when nothing sends it
|
|
179
|
+
const senderHomes = new Map<string, string | undefined>();
|
|
180
|
+
const senderHome = (nodeId: string): string | undefined => {
|
|
181
|
+
if (!senderHomes.has(nodeId)) {
|
|
182
|
+
const senders = new Set((sendersOf.get(nodeId) ?? []).filter((sender) => !carries(sender)).map(representative));
|
|
183
|
+
const [sender] = [...senders];
|
|
184
|
+
senderHomes.set(nodeId, senders.size > 1 ? nodeId : sender);
|
|
185
|
+
}
|
|
186
|
+
return senderHomes.get(nodeId);
|
|
187
|
+
};
|
|
188
|
+
|
|
126
189
|
// The node something is shown as: its system, the card of the subdomain it's
|
|
127
190
|
// in, the (sub)domain it's in, or itself
|
|
128
191
|
const representative = (nodeId: string): string => {
|
|
129
192
|
const boxes = domainMessages.get(nodeId);
|
|
130
193
|
if (boxes?.size === 1) return representative([...boxes][0]);
|
|
194
|
+
const home = isMessage(nodeId) ? senderHome(nodeId) : undefined;
|
|
195
|
+
if (home) return home;
|
|
131
196
|
for (let node = nodesById.get(nodeId); node; node = node.parentId ? nodesById.get(node.parentId) : undefined) {
|
|
132
197
|
const system: any = systemsByGroupId.get(node.id);
|
|
133
198
|
if (system) return generateIdForNode(system);
|
|
@@ -149,6 +214,29 @@ export const collapseSystems = (
|
|
|
149
214
|
);
|
|
150
215
|
const shownNodes = new Map([...otherNodes, ...systemNodes, ...cards.values()].map((node) => [node.id, node]));
|
|
151
216
|
|
|
217
|
+
// The messages sent through a channel (and the channels before it)
|
|
218
|
+
const messagesThrough = (channelId: string, seen = new Set<string>()): string[] => {
|
|
219
|
+
if (seen.has(channelId)) return [];
|
|
220
|
+
seen.add(channelId);
|
|
221
|
+
return (sendersOf.get(channelId) ?? []).flatMap((sender) =>
|
|
222
|
+
isMessage(sender) ? [sender] : isChannel(sender) ? messagesThrough(sender, seen) : []
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
const receives = (nodeId: string, messageId: string) =>
|
|
226
|
+
saysItReceives(nodesById.get(nodeId), nodesById.get(messageId), receivedBy.get(nodeId)) ?? false;
|
|
227
|
+
|
|
228
|
+
// Where an edge is from and the messages it carries. A channel is one node for
|
|
229
|
+
// all its messages, so an edge out of one is from each message sent through it
|
|
230
|
+
// that its target receives (all of them when that isn't known), as those
|
|
231
|
+
// messages are shown. Edges into a channel are shown by the edges out of it
|
|
232
|
+
const edgeSources = (edge: any): { from: string; messages: string[] }[] => {
|
|
233
|
+
if (isChannel(edge.target) && carries(edge.source)) return [];
|
|
234
|
+
const through = isChannel(edge.source) ? messagesThrough(edge.source) : [];
|
|
235
|
+
if (through.length === 0) return [{ from: edge.source, messages: [edge.source, edge.target].filter(isMessage) }];
|
|
236
|
+
const sent = through.filter((messageId) => receives(edge.target, messageId));
|
|
237
|
+
return (sent.length > 0 ? sent : through).map((messageId) => ({ from: messageId, messages: [messageId] }));
|
|
238
|
+
};
|
|
239
|
+
|
|
152
240
|
// Edges between systems / actors / domains: context relationships as they
|
|
153
241
|
// are, edges to nodes kept as their first label, the rest labelled with the
|
|
154
242
|
// messages they carry
|
|
@@ -156,28 +244,30 @@ export const collapseSystems = (
|
|
|
156
244
|
string,
|
|
157
245
|
{ source: string; target: string; messages: Map<string, string>; relationship?: any; label?: string; crossDomain?: boolean }
|
|
158
246
|
>();
|
|
159
|
-
graph.edges.forEach((edge) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
247
|
+
graph.edges.forEach((edge) =>
|
|
248
|
+
edgeSources(edge).forEach(({ from, messages }) => {
|
|
249
|
+
const source = representative(from);
|
|
250
|
+
const target = representative(edge.target);
|
|
251
|
+
if (
|
|
252
|
+
source === target ||
|
|
253
|
+
!shownNodes.has(source) ||
|
|
254
|
+
!shownNodes.has(target) ||
|
|
255
|
+
// One shown node inside another (e.g. a system in its domain)
|
|
256
|
+
isInside(shownNodes, source, target) ||
|
|
257
|
+
isInside(shownNodes, target, source)
|
|
258
|
+
)
|
|
259
|
+
return;
|
|
260
|
+
const key = `${source}->${target}`;
|
|
261
|
+
const connection = connections.get(key) ?? { source, target, messages: new Map<string, string>() };
|
|
262
|
+
if (edge.id.startsWith(CONTEXT_EDGE_PREFIX)) connection.relationship = edge;
|
|
263
|
+
if (edge.data?.crossDomain) connection.crossDomain = true;
|
|
264
|
+
messages.forEach((messageId) => {
|
|
265
|
+
const node = nodesById.get(messageId);
|
|
266
|
+
connection.messages.set(node.data?.message?.name ?? messageId, node.type);
|
|
267
|
+
});
|
|
268
|
+
connections.set(key, connection);
|
|
269
|
+
})
|
|
270
|
+
);
|
|
181
271
|
|
|
182
272
|
// A relationship already connects the pair (either way), so skip messages
|
|
183
273
|
const related = new Set(
|
|
@@ -222,7 +312,7 @@ export const buildDomainTree = (domain: any, findDomain: (id: string, version?:
|
|
|
222
312
|
return tree;
|
|
223
313
|
};
|
|
224
314
|
|
|
225
|
-
/** Which domain (and system) each service belongs to, and the domain each subdomain is in */
|
|
315
|
+
/** Which domain (and system) each service or agent belongs to, and the domain each subdomain is in */
|
|
226
316
|
export const collectOwners = (domains: any[]) => {
|
|
227
317
|
const owners: Owners = new Map();
|
|
228
318
|
const parentDomainOf = new Map<string, any>();
|
|
@@ -234,9 +324,9 @@ export const collectOwners = (domains: any[]) => {
|
|
|
234
324
|
if (!parentDomainOf.has(subdomain.data.id)) parentDomainOf.set(subdomain.data.id, d);
|
|
235
325
|
});
|
|
236
326
|
});
|
|
237
|
-
// Services outside systems, unless a system already claims them
|
|
327
|
+
// Services outside systems, unless a system already claims them, and agents (systems don't have them)
|
|
238
328
|
domains.forEach((d: any) =>
|
|
239
|
-
(d.data.services || []).forEach((service: any) => {
|
|
329
|
+
[...(d.data.services || []), ...(d.data.agents || [])].forEach((service: any) => {
|
|
240
330
|
const serviceId = generateIdForNode(service);
|
|
241
331
|
if (!owners.has(serviceId)) owners.set(serviceId, { domain: d });
|
|
242
332
|
})
|
|
@@ -298,8 +388,10 @@ export const buildDomainGraph = ({
|
|
|
298
388
|
])
|
|
299
389
|
);
|
|
300
390
|
const edges: any[] = [];
|
|
301
|
-
// Nodes replaced by another (services in other domains become the domain)
|
|
391
|
+
// Nodes replaced by another (services in other domains become the domain),
|
|
392
|
+
// and what each of them receives
|
|
302
393
|
const replacedBy = new Map<string, string>();
|
|
394
|
+
const replacedReceives = new Map<string, any[]>();
|
|
303
395
|
|
|
304
396
|
// Other domains are shown as a single card, with counts of what's in them. A
|
|
305
397
|
// subdomain of another domain is shown inside a box for that domain (and
|
|
@@ -364,6 +456,7 @@ export const buildDomainGraph = ({
|
|
|
364
456
|
const owner = SERVICE_TYPES.has(node.type) ? owners.get(node.id) : undefined;
|
|
365
457
|
if (owner && !groupOfDomain.has(owner.domain.data.id)) {
|
|
366
458
|
replacedBy.set(node.id, addOtherDomain(owner.domain));
|
|
459
|
+
replacedReceives.set(node.id, (node.data?.service ?? node.data?.agent)?.receives ?? []);
|
|
367
460
|
return;
|
|
368
461
|
}
|
|
369
462
|
if (owner && owner.system) {
|
|
@@ -384,7 +477,12 @@ export const buildDomainGraph = ({
|
|
|
384
477
|
}
|
|
385
478
|
// Messages, channels, data stores: kept in the first graph they appear in
|
|
386
479
|
if (nodes.has(node.id)) return;
|
|
387
|
-
|
|
480
|
+
// A service or agent in no domain isn't in the system whose graph it came from
|
|
481
|
+
if (SERVICE_TYPES.has(node.type)) {
|
|
482
|
+
nodes.set(node.id, { ...node, parentId: undefined, extent: undefined });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
nodes.set(node.id, node.parentId || !inside ? node : { ...node, parentId: inside });
|
|
388
486
|
});
|
|
389
487
|
|
|
390
488
|
systems.forEach(({ system, groupId: domainGroup }, index) => {
|
|
@@ -438,13 +536,19 @@ export const buildDomainGraph = ({
|
|
|
438
536
|
if (boxId) replacedBy.set(replaced, boxId);
|
|
439
537
|
});
|
|
440
538
|
|
|
441
|
-
const graphEdges = connectEdges(edges, new Map([...cardBoxes, ...replacedBy]), nodes);
|
|
442
|
-
|
|
539
|
+
const graphEdges = dropReceivesThroughChannels(connectEdges(edges, new Map([...cardBoxes, ...replacedBy]), nodes), nodes);
|
|
540
|
+
placeMessagesWithOwners(nodes, graphEdges, new Set(domainMessages.keys()));
|
|
541
|
+
// What the services each other domain stands for receive, by the node shown for the domain
|
|
542
|
+
const receivedBy = new Map<string, any[]>();
|
|
543
|
+
replacedBy.forEach((nodeId, replaced) =>
|
|
544
|
+
receivedBy.set(nodeId, [...(receivedBy.get(nodeId) ?? []), ...(replacedReceives.get(replaced) ?? [])])
|
|
545
|
+
);
|
|
443
546
|
return {
|
|
444
547
|
nodes,
|
|
445
548
|
edges: graphEdges,
|
|
446
549
|
otherDomainIds: new Set([...replacedBy.values(), ...[...otherDomainIds].map((id) => cardBoxes.get(id) ?? id)]),
|
|
447
550
|
domainMessages,
|
|
551
|
+
receivedBy,
|
|
448
552
|
};
|
|
449
553
|
};
|
|
450
554
|
|
|
@@ -480,14 +584,6 @@ const addDomainMessages = ({
|
|
|
480
584
|
const treeDomainIds = new Set(tree.map((entry) => entry.domain.data.id));
|
|
481
585
|
const otherDomainIds = new Set<string>();
|
|
482
586
|
const domainMessages = new Map<string, Set<string>>();
|
|
483
|
-
// The domain each agent is in (owners only has services): the tree's versions first
|
|
484
|
-
const agentOwners = new Map<string, any>();
|
|
485
|
-
[...tree.map((entry) => entry.domain), ...domains].forEach((domain) =>
|
|
486
|
-
(domain.data.agents || []).forEach((agent: any) => {
|
|
487
|
-
const agentId = generateIdForNode(agent);
|
|
488
|
-
if (!agentOwners.has(agentId)) agentOwners.set(agentId, domain);
|
|
489
|
-
})
|
|
490
|
-
);
|
|
491
587
|
// Whether a domain or service sends (or receives) the message (their messages are already looked up)
|
|
492
588
|
const lists = (resource: any, key: 'sends' | 'receives', messageId: string) =>
|
|
493
589
|
(resource.data[key] || []).some((message: any) => message.data && generateIdForNode(message) === messageId);
|
|
@@ -513,19 +609,19 @@ const addDomainMessages = ({
|
|
|
513
609
|
return false;
|
|
514
610
|
};
|
|
515
611
|
// Whether a service in a (sub)domain's box says it sends (or receives) the message: e.g. one it sends in a
|
|
516
|
-
// message group, which its graph shows as the group rather than the message. Services only:
|
|
517
|
-
//
|
|
612
|
+
// message group, which its graph shows as the group rather than the message. Services only: a domain's
|
|
613
|
+
// agents are only drawn when another graph brings them in
|
|
518
614
|
const groupOfTreeDomain = new Map(tree.map((entry) => [entry.domain.data.id, entry.groupId]));
|
|
519
615
|
const declaredIn = (groupId: string, key: 'sends' | 'receives', messageId: string) =>
|
|
520
616
|
catalogServices.some((service) => {
|
|
521
|
-
if (!lists(service, key, messageId)) return false;
|
|
617
|
+
if (service.collection === 'agents' || !lists(service, key, messageId)) return false;
|
|
522
618
|
const ownerGroup = groupOfTreeDomain.get(owners.get(generateIdForNode(service))?.domain.data.id);
|
|
523
619
|
return !!ownerGroup && (ownerGroup === groupId || isInside(nodes, ownerGroup, groupId));
|
|
524
620
|
});
|
|
525
621
|
// The node standing for a service (or agent) on the other side, or none when it's in the domain
|
|
526
622
|
const otherSide = (service: any) => {
|
|
527
623
|
const serviceId = generateIdForNode(service);
|
|
528
|
-
const ownerDomain = owners.get(serviceId)?.domain
|
|
624
|
+
const ownerDomain = owners.get(serviceId)?.domain;
|
|
529
625
|
if (ownerDomain) return treeDomainIds.has(ownerDomain.data.id) ? undefined : addOther(ownerDomain);
|
|
530
626
|
if (!nodes.has(serviceId)) {
|
|
531
627
|
nodes.set(serviceId, {
|
|
@@ -609,27 +705,77 @@ const addDomainMessages = ({
|
|
|
609
705
|
/**
|
|
610
706
|
* Messages are placed in the first graph they appear in, which can be a
|
|
611
707
|
* system consuming them. Moves each message into the system (or domain) of the
|
|
612
|
-
* services
|
|
708
|
+
* services it belongs to, when they're all in the same one: an event's
|
|
709
|
+
* publishers, a command's or query's receivers (through any channels). Each
|
|
710
|
+
* channel goes with the messages sent through it. Messages a domain
|
|
613
711
|
* sends or receives itself (`kept`) stay outside its box, where its edge to them
|
|
614
712
|
* is drawn.
|
|
615
713
|
*/
|
|
616
|
-
export const
|
|
617
|
-
const
|
|
714
|
+
export const placeMessagesWithOwners = (nodes: Map<string, any>, edges: any[], kept: Set<string> = new Set()) => {
|
|
715
|
+
const ownerGroups = new Map<string, Set<string | undefined>>();
|
|
716
|
+
const addOwner = (messageId: string, owner: any) =>
|
|
717
|
+
ownerGroups.set(messageId, (ownerGroups.get(messageId) ?? new Set()).add(owner.parentId));
|
|
718
|
+
const receiversOf = new Map<string, string[]>();
|
|
719
|
+
edges.forEach((edge) => receiversOf.set(edge.source, [...(receiversOf.get(edge.source) ?? []), edge.target]));
|
|
618
720
|
edges.forEach((edge) => {
|
|
619
721
|
const publisher = nodes.get(edge.source);
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
722
|
+
const type = nodes.get(edge.target)?.type;
|
|
723
|
+
if (!MESSAGE_TYPES.has(type) || RECEIVER_OWNED_TYPES.has(type) || !SERVICE_TYPES.has(publisher?.type)) return;
|
|
724
|
+
addOwner(edge.target, publisher);
|
|
725
|
+
});
|
|
726
|
+
nodes.forEach((message, messageId) => {
|
|
727
|
+
if (!RECEIVER_OWNED_TYPES.has(message.type)) return;
|
|
728
|
+
receiversThroughChannels(messageId, nodes, receiversOf)
|
|
729
|
+
.filter((nodeId) => SERVICE_TYPES.has(nodes.get(nodeId)?.type))
|
|
730
|
+
.forEach((nodeId) => addOwner(messageId, nodes.get(nodeId)));
|
|
624
731
|
});
|
|
625
|
-
|
|
732
|
+
ownerGroups.forEach((groups, messageId) => {
|
|
626
733
|
if (kept.has(messageId)) return;
|
|
627
734
|
const [parentId] = [...groups];
|
|
628
735
|
const message = nodes.get(messageId);
|
|
629
|
-
|
|
736
|
+
// Owners are drawn nodes, so no parent means outside every box (e.g. a service or agent in no domain)
|
|
737
|
+
if (groups.size === 1 && message.parentId !== parentId) {
|
|
630
738
|
nodes.set(messageId, { ...message, parentId, extent: undefined });
|
|
631
739
|
}
|
|
632
740
|
});
|
|
741
|
+
|
|
742
|
+
// Channels go with what sends through them (messages, or channels before them), when that's all in one place
|
|
743
|
+
const sendersOf = new Map<string, string[]>();
|
|
744
|
+
edges.forEach((edge) => sendersOf.set(edge.target, [...(sendersOf.get(edge.target) ?? []), edge.source]));
|
|
745
|
+
for (let moved = true; moved; ) {
|
|
746
|
+
moved = false;
|
|
747
|
+
nodes.forEach((channel, channelId) => {
|
|
748
|
+
if (channel.type !== 'channels' || kept.has(channelId)) return;
|
|
749
|
+
const senders = (sendersOf.get(channelId) ?? []).filter((sender) => nodes.has(sender));
|
|
750
|
+
const groups = new Set(senders.map((sender) => nodes.get(sender).parentId));
|
|
751
|
+
const [parentId] = [...groups];
|
|
752
|
+
if (groups.size === 1 && channel.parentId !== parentId) {
|
|
753
|
+
nodes.set(channelId, { ...channel, parentId, extent: undefined });
|
|
754
|
+
moved = true;
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Drops the edges drawn for domains receiving a message themselves (`domain-receives-`)
|
|
762
|
+
* when the message already reaches them through its channels, e.g. another
|
|
763
|
+
* domain listing a command in its `receives` whose service receives it from a
|
|
764
|
+
* queue: the domain is drawn receiving it from the queue, not from the command too.
|
|
765
|
+
*/
|
|
766
|
+
const dropReceivesThroughChannels = (edges: any[], nodes: Map<string, any>) => {
|
|
767
|
+
const isChannel = (nodeId: string) => nodes.get(nodeId)?.type === 'channels';
|
|
768
|
+
const reachesThroughChannels = (messageId: string, targetId: string) => {
|
|
769
|
+
const seen = new Set<string>();
|
|
770
|
+
for (let current = [messageId]; current.length > 0; ) {
|
|
771
|
+
const next = edges.filter((edge) => current.includes(edge.source) && isChannel(edge.target) && !seen.has(edge.target));
|
|
772
|
+
if (next.some((edge) => edges.some((out) => out.source === edge.target && out.target === targetId))) return true;
|
|
773
|
+
next.forEach((edge) => seen.add(edge.target));
|
|
774
|
+
current = next.map((edge) => edge.target);
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
};
|
|
778
|
+
return edges.filter((edge) => !edge.id.startsWith('domain-receives-') || !reachesThroughChannels(edge.source, edge.target));
|
|
633
779
|
};
|
|
634
780
|
|
|
635
781
|
/**
|
|
@@ -683,15 +829,19 @@ export const addContext = (contexts: Graph[], systems: { system: any }[], nodes:
|
|
|
683
829
|
};
|
|
684
830
|
|
|
685
831
|
/**
|
|
686
|
-
* Marks cross-domain communication.
|
|
687
|
-
* send to (e.g.
|
|
832
|
+
* Marks cross-domain communication. Events and channels only other domains
|
|
833
|
+
* send to (e.g. an event this domain consumes, and the topic it comes
|
|
688
834
|
* through) are another domain's: they sit outside this domain, between the
|
|
689
835
|
* domain sending them and their consumers, rather than in the system that
|
|
690
|
-
* consumes them.
|
|
836
|
+
* consumes them. Commands and queries belong to what receives them, so those
|
|
837
|
+
* (and the channels only they're sent through) only other domains receive are
|
|
838
|
+
* another domain's, and those this domain receives are its own, whoever sends
|
|
839
|
+
* them. Messages and channels other domains send, and this domain's that reach
|
|
691
840
|
* other domains, are on a cross-domain path (`data.crossDomain`), as are the
|
|
692
841
|
* edges to and from them and other domains.
|
|
693
842
|
*/
|
|
694
843
|
export const markCrossDomain = (graph: Graph, otherDomainIds: Set<string>): Graph => {
|
|
844
|
+
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
695
845
|
const sendersOf = new Map<string, string[]>();
|
|
696
846
|
const receiversOf = new Map<string, string[]>();
|
|
697
847
|
graph.edges.forEach((edge) => {
|
|
@@ -718,16 +868,45 @@ export const markCrossDomain = (graph: Graph, otherDomainIds: Set<string>): Grap
|
|
|
718
868
|
return reached;
|
|
719
869
|
};
|
|
720
870
|
// Sent only by other domains (or their messages and channels)
|
|
721
|
-
const
|
|
871
|
+
const sentByOthers = spread(new Set(otherDomainIds), (nodeId, reached) => {
|
|
722
872
|
const senders = sendersOf.get(nodeId) || [];
|
|
723
873
|
return senders.length > 0 && senders.every((sender) => reached.has(sender));
|
|
724
874
|
});
|
|
875
|
+
// Received only by other domains (or their messages and channels)
|
|
876
|
+
const receivedByOthers = spread(new Set(otherDomainIds), (nodeId, reached) => {
|
|
877
|
+
const receivers = receiversOf.get(nodeId) || [];
|
|
878
|
+
return receivers.length > 0 && receivers.every((receiver) => reached.has(receiver));
|
|
879
|
+
});
|
|
725
880
|
// Reaching other domains (directly, or through messages and channels that do)
|
|
726
|
-
const outgoing = spread(new Set(
|
|
881
|
+
const outgoing = spread(new Set(sentByOthers), (nodeId, reached) =>
|
|
727
882
|
(receiversOf.get(nodeId) || []).some((receiver) => reached.has(receiver))
|
|
728
883
|
);
|
|
729
884
|
|
|
730
|
-
|
|
885
|
+
// A command or query, or a channel only they're sent through (by channel, worked out once: routes can split and
|
|
886
|
+
// join again)
|
|
887
|
+
const receiverOwnedChannels = new Map<string, boolean>();
|
|
888
|
+
const isReceiverOwned = (nodeId: string): boolean => {
|
|
889
|
+
const node = nodesById.get(nodeId);
|
|
890
|
+
if (MESSAGE_TYPES.has(node?.type)) return RECEIVER_OWNED_TYPES.has(node.type);
|
|
891
|
+
if (node?.type !== 'channels') return false;
|
|
892
|
+
if (!receiverOwnedChannels.has(nodeId)) {
|
|
893
|
+
// Not the receiver's while it's being worked out, so a loop of channels ends
|
|
894
|
+
receiverOwnedChannels.set(nodeId, false);
|
|
895
|
+
const carried = (sendersOf.get(nodeId) || []).filter((sender) => nodesById.has(sender) && isCarrier(nodesById.get(sender)));
|
|
896
|
+
receiverOwnedChannels.set(nodeId, carried.length > 0 && carried.every(isReceiverOwned));
|
|
897
|
+
}
|
|
898
|
+
return receiverOwnedChannels.get(nodeId)!;
|
|
899
|
+
};
|
|
900
|
+
// A command or query only other domains receive (not counting consumers of other messages on its channels)
|
|
901
|
+
const receivedOnlyByOthers = (messageId: string) => {
|
|
902
|
+
const receivers = receiversThroughChannels(messageId, nodesById, receiversOf);
|
|
903
|
+
return receivers.length > 0 && receivers.every((receiver) => otherDomainIds.has(receiver));
|
|
904
|
+
};
|
|
905
|
+
const isExternalCarrier = (node: any) => {
|
|
906
|
+
if (!isCarrier(node) || otherDomainIds.has(node.id)) return false;
|
|
907
|
+
if (RECEIVER_OWNED_TYPES.has(node.type)) return receivedOnlyByOthers(node.id);
|
|
908
|
+
return (isReceiverOwned(node.id) ? receivedByOthers : sentByOthers).has(node.id);
|
|
909
|
+
};
|
|
731
910
|
const onCrossDomainPath = (nodeId: string) => outgoing.has(nodeId);
|
|
732
911
|
return {
|
|
733
912
|
nodes: graph.nodes.map((node) => {
|
|
@@ -775,11 +954,8 @@ export const getNodesAndEdges = (props: NodesAndEdgesProps): Promise<DomainLevel
|
|
|
775
954
|
* or its latest when no domain has it: who sends or receives a domain's own
|
|
776
955
|
* messages is matched against these.
|
|
777
956
|
*/
|
|
778
|
-
const inVersionsUsed = (resources: any[],
|
|
779
|
-
const used = new Set(
|
|
780
|
-
...owners.keys(),
|
|
781
|
-
...domains.flatMap((domain) => (domain.data.agents || []).map((agent: any) => generateIdForNode(agent))),
|
|
782
|
-
]);
|
|
957
|
+
const inVersionsUsed = (resources: any[], owners: Owners) => {
|
|
958
|
+
const used = new Set(owners.keys());
|
|
783
959
|
const key = (resource: any) => `${resource.collection}:${resource.data.id}`;
|
|
784
960
|
const usedKeys = new Set(resources.filter((resource) => used.has(generateIdForNode(resource))).map(key));
|
|
785
961
|
return resources.filter((resource) =>
|
|
@@ -809,11 +985,11 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
809
985
|
const tree = buildDomainTree(domain, findDomain);
|
|
810
986
|
// The tree's domains last, so the versions shown own their services
|
|
811
987
|
const { owners, parentDomainOf } = collectOwners([...domains, ...tree.map((entry) => entry.domain)]);
|
|
812
|
-
// A service the domain
|
|
813
|
-
// even when a system in another domain has it too
|
|
988
|
+
// A service (or agent) the domain or a subdomain lists itself is shown as its
|
|
989
|
+
// own, even when a system in another domain has it too
|
|
814
990
|
const treeDomainIds = new Set(tree.map((entry) => entry.domain.data.id));
|
|
815
991
|
tree.forEach(({ domain: d }) =>
|
|
816
|
-
(d.data.services || []).forEach((service: any) => {
|
|
992
|
+
[...(d.data.services || []), ...(d.data.agents || [])].forEach((service: any) => {
|
|
817
993
|
const serviceId = generateIdForNode(service);
|
|
818
994
|
if (!treeDomainIds.has(owners.get(serviceId)?.domain.data.id)) owners.set(serviceId, { domain: d });
|
|
819
995
|
})
|
|
@@ -860,7 +1036,7 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
860
1036
|
),
|
|
861
1037
|
]);
|
|
862
1038
|
|
|
863
|
-
const { nodes, edges, otherDomainIds, domainMessages } = buildDomainGraph({
|
|
1039
|
+
const { nodes, edges, otherDomainIds, domainMessages, receivedBy } = buildDomainGraph({
|
|
864
1040
|
tree,
|
|
865
1041
|
owners,
|
|
866
1042
|
parentDomainOf,
|
|
@@ -870,7 +1046,7 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
870
1046
|
serviceGraphs,
|
|
871
1047
|
mode,
|
|
872
1048
|
domains,
|
|
873
|
-
catalogServices: inVersionsUsed([...services, ...agents],
|
|
1049
|
+
catalogServices: inVersionsUsed([...services, ...agents], owners),
|
|
874
1050
|
});
|
|
875
1051
|
const context = addContext(contexts, systems, nodes);
|
|
876
1052
|
const graph = { nodes: [...nodes.values(), ...context.nodes], edges: [...edges, ...context.edges] };
|
|
@@ -881,14 +1057,15 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
881
1057
|
// by services not in one) are hidden too, with the nodes either side connected
|
|
882
1058
|
// directly. A domain without systems or subdomains has no level 1.
|
|
883
1059
|
if (systems.length === 0 && tree.length === 1) return layoutLevels(detailedGraph);
|
|
884
|
-
// Built from the graph before
|
|
885
|
-
//
|
|
1060
|
+
// Built from the graph before other domains' messages were moved out of the
|
|
1061
|
+
// systems they were drawn in, so they collapse into those systems
|
|
886
1062
|
const collapsed = collapseSystems(
|
|
887
1063
|
{ ...graph, edges: detailedGraph.edges },
|
|
888
1064
|
systems.map(({ system }) => system),
|
|
889
1065
|
context.contextNodesById,
|
|
890
1066
|
new Map(tree.filter((entry) => entry.parentId).map((entry) => [entry.groupId, entry.domain])),
|
|
891
|
-
domainMessages
|
|
1067
|
+
domainMessages,
|
|
1068
|
+
receivedBy
|
|
892
1069
|
);
|
|
893
1070
|
const overview = hideMessageNodes(collapsed.nodes, collapsed.edges);
|
|
894
1071
|
|