@likec4/core 1.17.1 → 1.18.0

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.
Files changed (112) hide show
  1. package/dist/compute-view/index.d.mts +91 -0
  2. package/dist/compute-view/index.d.ts +91 -0
  3. package/dist/compute-view/index.mjs +79 -0
  4. package/dist/index.d.mts +246 -434
  5. package/dist/index.d.ts +246 -434
  6. package/dist/index.mjs +418 -1322
  7. package/dist/model/index.d.mts +642 -0
  8. package/dist/model/index.d.ts +642 -0
  9. package/dist/model/index.mjs +2 -0
  10. package/dist/shared/core.BBgiAAv1.mjs +144 -0
  11. package/dist/shared/core.BPyJvuMU.mjs +2420 -0
  12. package/dist/shared/core.BQ4AbfvB.mjs +9228 -0
  13. package/dist/shared/{core.CoLQkcYL.d.mts → core.DMYY7C8s.d.mts} +398 -249
  14. package/dist/shared/{core.CoLQkcYL.d.ts → core.DMYY7C8s.d.ts} +398 -249
  15. package/dist/shared/core.k12s_keD.mjs +203 -0
  16. package/dist/types/index.d.mts +69 -1
  17. package/dist/types/index.d.ts +69 -1
  18. package/dist/types/index.mjs +21 -1
  19. package/package.json +38 -40
  20. package/src/builder/Builder.deployment.ts +219 -0
  21. package/src/builder/Builder.deploymentModel.ts +360 -0
  22. package/src/builder/Builder.element.ts +17 -19
  23. package/src/builder/Builder.model.ts +13 -2
  24. package/src/builder/Builder.test-d.ts +59 -4
  25. package/src/builder/Builder.ts +238 -53
  26. package/src/builder/Builder.view.ts +238 -53
  27. package/src/builder/_types.ts +87 -15
  28. package/src/compute-view/LikeC4ModelGraph.ts +330 -0
  29. package/src/compute-view/compute-view.ts +92 -0
  30. package/src/compute-view/deployment-view/Memory.ts +108 -0
  31. package/src/compute-view/deployment-view/Stage.ts +145 -0
  32. package/src/compute-view/deployment-view/__test__/fixture.ts +210 -0
  33. package/src/compute-view/deployment-view/_types.ts +29 -0
  34. package/src/compute-view/deployment-view/clean-connections.ts +229 -0
  35. package/src/compute-view/deployment-view/compute.ts +111 -0
  36. package/src/compute-view/deployment-view/predicates/deployment-ref.ts +146 -0
  37. package/src/compute-view/deployment-view/predicates/relation-direct.ts +130 -0
  38. package/src/compute-view/deployment-view/predicates/relation-in-out.ts +43 -0
  39. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +46 -0
  40. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +49 -0
  41. package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
  42. package/src/compute-view/deployment-view/utils.ts +212 -0
  43. package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
  44. package/src/compute-view/dynamic-view/compute.ts +310 -0
  45. package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +456 -0
  46. package/src/compute-view/element-view/__test__/fixture.ts +645 -0
  47. package/src/compute-view/element-view/compute.ts +734 -0
  48. package/src/compute-view/element-view/predicates.ts +517 -0
  49. package/src/compute-view/index.ts +5 -0
  50. package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
  51. package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
  52. package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
  53. package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
  54. package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
  55. package/src/compute-view/utils/buildComputedNodes.ts +148 -0
  56. package/src/compute-view/utils/buildElementNotations.ts +63 -0
  57. package/src/compute-view/utils/derive-edge-props-from-relationships.ts +97 -0
  58. package/src/compute-view/utils/elementExpressionToPredicate.ts +52 -0
  59. package/src/compute-view/utils/link-nodes-with-edges.ts +58 -0
  60. package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
  61. package/src/compute-view/utils/resolve-extended-views.ts +65 -0
  62. package/src/compute-view/utils/resolve-global-rules.ts +83 -0
  63. package/src/compute-view/utils/sortNodes.ts +101 -0
  64. package/src/compute-view/utils/topological-sort.ts +160 -0
  65. package/src/compute-view/utils/uniqueTags.test.ts +42 -0
  66. package/src/compute-view/utils/uniqueTags.ts +21 -0
  67. package/src/compute-view/utils/view-hash.ts +27 -0
  68. package/src/compute-view/utils/with-readable-edges.ts +31 -0
  69. package/src/index.ts +5 -1
  70. package/src/model/DeploymentElementModel.ts +534 -0
  71. package/src/model/DeploymentModel.ts +364 -0
  72. package/src/model/ElementModel.ts +195 -0
  73. package/src/model/LikeC4Model.test-d.ts +151 -0
  74. package/src/model/LikeC4Model.ts +280 -457
  75. package/src/model/RelationModel.ts +84 -0
  76. package/src/model/__test__/fixture.ts +153 -338
  77. package/src/model/connection/ConnectionModel.ts +94 -0
  78. package/src/model/connection/DeploymentConnectionModel.ts +99 -0
  79. package/src/model/connection/deployment.ts +124 -0
  80. package/src/model/connection/model.ts +123 -0
  81. package/src/model/index.ts +4 -2
  82. package/src/model/types.ts +95 -15
  83. package/src/model/view/EdgeModel.ts +103 -0
  84. package/src/model/view/LikeC4ViewModel.ts +220 -0
  85. package/src/model/view/NodeModel.ts +228 -0
  86. package/src/types/_common.ts +2 -0
  87. package/src/types/deployments.ts +237 -0
  88. package/src/types/element.ts +7 -3
  89. package/src/types/expression.ts +8 -6
  90. package/src/types/index.ts +1 -0
  91. package/src/types/model.ts +68 -9
  92. package/src/types/overview-graph.ts +2 -2
  93. package/src/types/relation.ts +4 -4
  94. package/src/types/view-notation.ts +2 -2
  95. package/src/types/view.ts +104 -30
  96. package/src/utils/{commonHead.ts → common-head.ts} +2 -0
  97. package/src/utils/fqn.ts +83 -27
  98. package/src/utils/getOrCreate.ts +42 -0
  99. package/src/utils/graphlib.ts +10 -0
  100. package/src/utils/index.ts +4 -1
  101. package/src/utils/object-hash.ts +3 -0
  102. package/src/utils/set.ts +76 -0
  103. package/src/utils/string-hash.ts +16 -0
  104. package/dist/index.cjs +0 -5452
  105. package/dist/index.d.cts +0 -1053
  106. package/dist/shared/core.BXTs-8n7.mjs +0 -293
  107. package/dist/shared/core.CoLQkcYL.d.cts +0 -734
  108. package/dist/shared/core.DIYZvpVg.cjs +0 -353
  109. package/dist/types/index.cjs +0 -70
  110. package/dist/types/index.d.cts +0 -2
  111. package/src/model/LikeC4DiagramModel.ts +0 -338
  112. package/src/model/LikeC4ViewModel.ts +0 -320
@@ -0,0 +1,330 @@
1
+ import { isArray, isString, reverse } from 'remeda'
2
+ import { invariant } from '../errors'
3
+ import { type Element, type Fqn, type ModelGlobals, type Relation, type RelationId } from '../types'
4
+ import { ancestorsFqn, commonAncestor, getOrCreate, isSameHierarchy, parentFqn } from '../utils'
5
+ import { intersection } from '../utils/set'
6
+
7
+ export type Params = {
8
+ elements: Record<Fqn, Element>
9
+ relations: Record<RelationId, Relation>
10
+ // Optional for tests
11
+ globals?: ModelGlobals
12
+ }
13
+
14
+ type RelationEdge = {
15
+ source: Element
16
+ target: Element
17
+ relations: Relation[]
18
+ }
19
+
20
+ type FqnOrElement = Fqn | Element
21
+ type FqnsOrElements = ReadonlyArray<Fqn> | ReadonlyArray<Element>
22
+
23
+ const RelationsSet = Set<Relation>
24
+ const MapRelations = Map<Fqn, Set<Relation>>
25
+ /**
26
+ * Used only for views calculations.
27
+ * Subject to change.
28
+ */
29
+ export class LikeC4ModelGraph {
30
+ readonly #elements = new Map<Fqn, Element>()
31
+ // Parent element for given FQN
32
+ readonly #parents = new Map<Fqn, Element>()
33
+ // Children elements for given FQN
34
+ readonly #children = new Map<Fqn, Element[]>()
35
+ readonly #rootElements = new Set<Element>()
36
+
37
+ readonly #relations = new Map<RelationId, Relation>()
38
+ // Incoming to an element or its descendants
39
+ readonly #incoming = new MapRelations()
40
+ // Outgoing from an element or its descendants
41
+ readonly #outgoing = new MapRelations()
42
+ // Relationships inside the element, among descendants
43
+ readonly #internal = new MapRelations()
44
+
45
+ readonly #cacheAscendingSiblings = new Map<Fqn, Element[]>()
46
+
47
+ public readonly globals: ModelGlobals
48
+
49
+ constructor(
50
+ { elements, relations, globals }: Params
51
+ ) {
52
+ this.globals = globals ?? {
53
+ predicates: {},
54
+ dynamicPredicates: {},
55
+ styles: {}
56
+ }
57
+ for (const el of Object.values(elements)) {
58
+ this.addElement(el)
59
+ }
60
+ for (const rel of Object.values(relations)) {
61
+ this.addRelation(rel)
62
+ }
63
+ }
64
+
65
+ get rootElements() {
66
+ return [...this.#rootElements]
67
+ }
68
+
69
+ get elements() {
70
+ return [...this.#elements.values()]
71
+ }
72
+
73
+ public element(id: Fqn) {
74
+ const el = this.#elements.get(id)
75
+ invariant(el, `Element ${id} not found`)
76
+ return el
77
+ }
78
+
79
+ public connectedRelations(id: Fqn) {
80
+ return [...this._incomingTo(id), ...this._outgoingFrom(id), ...this._internalOf(id)]
81
+ }
82
+
83
+ public children(id: Fqn) {
84
+ return this._childrenOf(id).slice()
85
+ }
86
+
87
+ public descendants(id: Fqn) {
88
+ const result: Element[] = []
89
+ this.descendantsDfs(id, result)
90
+ return result
91
+ }
92
+
93
+ private descendantsDfs(id: Fqn, result: Element[]) {
94
+ result.push(this.element(id))
95
+ this.children(id).forEach(c => this.descendantsDfs(c.id, result))
96
+ }
97
+
98
+ // Get all sibling (i.e. same parent)
99
+ public siblings(element: Fqn | Element) {
100
+ const id = isString(element) ? element : element.id
101
+ const parent = parentFqn(id)
102
+ const siblings = parent ? this._childrenOf(parent) : this.rootElements
103
+ return siblings.filter(e => e.id !== id)
104
+ }
105
+
106
+ /**
107
+ * Get all ancestor elements (i.e. parent, parent’s parent, etc.)
108
+ * (from closest to root)
109
+ */
110
+ public ancestors(element: Fqn | Element): Array<Element> {
111
+ let id = isString(element) ? element : element.id
112
+ const result = [] as Element[]
113
+ let parent
114
+ while (parent = this.#parents.get(id)) {
115
+ result.push(parent)
116
+ id = parent.id
117
+ }
118
+ return result as Array<Element>
119
+ }
120
+
121
+ /**
122
+ * Resolve siblings of the element and its ancestors
123
+ * (from closest to root)
124
+ */
125
+ public ascendingSiblings(element: Fqn | Element): ReadonlyArray<Element> {
126
+ const id = isString(element) ? element : element.id
127
+ return getOrCreate(this.#cacheAscendingSiblings, id, () => [
128
+ ...this.siblings(id),
129
+ ...this.ancestors(id).flatMap(a => this.siblings(a.id))
130
+ ])
131
+ }
132
+
133
+ public incoming(element: Fqn | Element): ReadonlySet<Relation> {
134
+ return this._incomingTo(isString(element) ? element : element.id)
135
+ }
136
+
137
+ public outgoing(element: Fqn | Element): ReadonlySet<Relation> {
138
+ return this._outgoingFrom(isString(element) ? element : element.id)
139
+ }
140
+
141
+ /**
142
+ * Resolve all RelationEdges between element and others (any direction)
143
+ */
144
+ public anyEdgesBetween(
145
+ _element: Fqn | Element,
146
+ others: ReadonlyArray<Fqn> | ReadonlyArray<Element>
147
+ ): Array<RelationEdge> {
148
+ if (others.length === 0) {
149
+ return []
150
+ }
151
+ const element = isString(_element) ? this.element(_element) : _element
152
+ const in_element = this._incomingTo(element.id)
153
+ const element_out = this._outgoingFrom(element.id)
154
+ if (in_element.size === 0 && element_out.size === 0) {
155
+ return []
156
+ }
157
+
158
+ const result = [] as Array<RelationEdge>
159
+ for (const _other of others) {
160
+ const other = isString(_other) ? this.element(_other) : _other
161
+ if (isSameHierarchy(element, other)) {
162
+ continue
163
+ }
164
+
165
+ if (element_out.size > 0) {
166
+ const outcoming = intersection(this._incomingTo(other.id), element_out)
167
+ // const outcoming = filter(in_other, isOutgoing)
168
+ if (outcoming.size > 0) {
169
+ result.push({
170
+ source: element,
171
+ target: other,
172
+ relations: [...outcoming]
173
+ })
174
+ }
175
+ }
176
+
177
+ if (in_element.size > 0) {
178
+ const incoming = intersection(this._outgoingFrom(other.id), in_element)
179
+ // const incoming = filter(other_out, isIncoming)
180
+ if (incoming.size > 0) {
181
+ result.push({
182
+ source: other,
183
+ target: element,
184
+ relations: [...incoming]
185
+ })
186
+ }
187
+ }
188
+ }
189
+ return result
190
+ }
191
+
192
+ /**
193
+ * Resolve all RelationEdges between elements (any direction)
194
+ */
195
+ public edgesWithin<T extends Fqn[] | Element[]>(elements: T): Array<RelationEdge> {
196
+ if (elements.length < 2) {
197
+ return []
198
+ }
199
+ return elements.reduce((acc, el, index, array) => {
200
+ // return acc if last element
201
+ if (index === array.length - 1) {
202
+ return acc
203
+ }
204
+ acc.push(...this.anyEdgesBetween(el, array.slice(index + 1) as T))
205
+ return acc
206
+ }, [] as RelationEdge[])
207
+ }
208
+
209
+ /**
210
+ * Get directed RelationEdge between source and target if exists
211
+ */
212
+ public edgesBetween(
213
+ _sources: FqnOrElement | FqnsOrElements,
214
+ _targets: FqnOrElement | FqnsOrElements
215
+ ) {
216
+ const sources = isArray(_sources) ? _sources : [_sources]
217
+ const targets = isArray(_targets) ? _targets : [_targets]
218
+ if (sources.length === 0 || targets.length === 0) {
219
+ return []
220
+ }
221
+
222
+ const result = [] as Array<RelationEdge>
223
+ for (const _source of sources) {
224
+ const source = isString(_source) ? this.element(_source) : _source
225
+ const outcoming = this._outgoingFrom(source.id)
226
+ if (outcoming.size === 0) {
227
+ continue
228
+ }
229
+ // const isSameAsOut = isIncludedIn(outcoming)
230
+
231
+ for (const _target of targets) {
232
+ const target = isString(_target) ? this.element(_target) : _target
233
+ if (isSameHierarchy(source, target)) {
234
+ continue
235
+ }
236
+ const incoming = this._incomingTo(target.id)
237
+ const relations = intersection(outcoming, incoming)
238
+ if (relations.size > 0) {
239
+ result.push({
240
+ source,
241
+ target,
242
+ relations: [...relations]
243
+ })
244
+ }
245
+ }
246
+ }
247
+ return result
248
+ }
249
+
250
+ private addElement(el: Element) {
251
+ if (this.#elements.has(el.id)) {
252
+ throw new Error(`Element ${el.id} already exists`)
253
+ }
254
+ this.#elements.set(el.id, el)
255
+ const parentId = parentFqn(el.id)
256
+ if (parentId) {
257
+ this.#parents.set(el.id, this.element(parentId))
258
+ this._childrenOf(parentId).push(el)
259
+ } else {
260
+ this.#rootElements.add(el)
261
+ }
262
+ }
263
+
264
+ private addRelation(rel: Relation) {
265
+ if (this.#relations.has(rel.id)) {
266
+ throw new Error(`Relation ${rel.id} already exists`)
267
+ }
268
+ this.#relations.set(rel.id, rel)
269
+ this._incomingTo(rel.target).add(rel)
270
+ this._outgoingFrom(rel.source).add(rel)
271
+
272
+ const relParent = commonAncestor(rel.source, rel.target)
273
+ // Process internal relationships
274
+ if (relParent) {
275
+ for (const ancestor of [relParent, ...ancestorsFqn(relParent)]) {
276
+ this._internalOf(ancestor).add(rel)
277
+ }
278
+ }
279
+ // Process source hierarchy
280
+ for (const sourceAncestor of ancestorsFqn(rel.source)) {
281
+ if (sourceAncestor === relParent) {
282
+ break
283
+ }
284
+ this._outgoingFrom(sourceAncestor).add(rel)
285
+ }
286
+ // Process target hierarchy
287
+ for (const targetAncestor of ancestorsFqn(rel.target)) {
288
+ if (targetAncestor === relParent) {
289
+ break
290
+ }
291
+ this._incomingTo(targetAncestor).add(rel)
292
+ }
293
+ }
294
+
295
+ private _childrenOf(id: Fqn) {
296
+ let children = this.#children.get(id)
297
+ if (!children) {
298
+ children = []
299
+ this.#children.set(id, children)
300
+ }
301
+ return children
302
+ }
303
+
304
+ private _incomingTo(id: Fqn) {
305
+ let incoming = this.#incoming.get(id)
306
+ if (!incoming) {
307
+ incoming = new RelationsSet()
308
+ this.#incoming.set(id, incoming)
309
+ }
310
+ return incoming
311
+ }
312
+
313
+ private _outgoingFrom(id: Fqn) {
314
+ let outgoing = this.#outgoing.get(id)
315
+ if (!outgoing) {
316
+ outgoing = new RelationsSet()
317
+ this.#outgoing.set(id, outgoing)
318
+ }
319
+ return outgoing
320
+ }
321
+
322
+ private _internalOf(id: Fqn) {
323
+ let internal = this.#internal.get(id)
324
+ if (!internal) {
325
+ internal = new RelationsSet()
326
+ this.#internal.set(id, internal)
327
+ }
328
+ return internal
329
+ }
330
+ }
@@ -0,0 +1,92 @@
1
+ import { mapValues } from 'remeda'
2
+ import type { SetOptional } from 'type-fest'
3
+ import { nonexhaustive } from '../errors'
4
+ import { LikeC4Model } from '../model'
5
+ import {
6
+ type ComputedLikeC4ModelFromParsed,
7
+ type ComputedView,
8
+ isDeploymentView,
9
+ isDynamicView,
10
+ isElementView,
11
+ type LikeC4View,
12
+ type ParsedLikeC4Model
13
+ } from '../types'
14
+ import { computeDeploymentView } from './deployment-view/compute'
15
+ import { DynamicViewComputeCtx } from './dynamic-view/compute'
16
+ import { ComputeCtx } from './element-view/compute'
17
+ import { LikeC4ModelGraph } from './LikeC4ModelGraph'
18
+
19
+ type ComputeViewResult<V extends ComputedView = ComputedView> =
20
+ | {
21
+ isSuccess: true
22
+ error?: undefined
23
+ view: V
24
+ }
25
+ | {
26
+ isSuccess: false
27
+ error: Error
28
+ view: undefined
29
+ }
30
+
31
+ type ComputeViewFn = <V extends LikeC4View>(viewsource: V) => ComputeViewResult<ComputedView>
32
+
33
+ type Params = SetOptional<ParsedLikeC4Model, 'views'>
34
+ export function mkComputeView(model: Params): ComputeViewFn {
35
+ const index = new LikeC4ModelGraph(model)
36
+ let likec4model: LikeC4Model
37
+
38
+ return function computeView(viewsource) {
39
+ try {
40
+ switch (true) {
41
+ case isDeploymentView(viewsource): {
42
+ likec4model ??= LikeC4Model.create({
43
+ ...model,
44
+ views: {}
45
+ })
46
+ return {
47
+ isSuccess: true,
48
+ view: computeDeploymentView(likec4model, viewsource)
49
+ }
50
+ }
51
+ case isDynamicView(viewsource):
52
+ return {
53
+ isSuccess: true,
54
+ view: DynamicViewComputeCtx.compute(viewsource, index)
55
+ }
56
+ case isElementView(viewsource):
57
+ return {
58
+ isSuccess: true,
59
+ view: ComputeCtx.elementView(viewsource, index)
60
+ }
61
+
62
+ default:
63
+ nonexhaustive(viewsource)
64
+ }
65
+ } catch (e) {
66
+ return {
67
+ isSuccess: false,
68
+ error: e instanceof Error ? e : new Error(`Unknown error: ${e}`),
69
+ view: undefined
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ export function computeViews<const P extends ParsedLikeC4Model<any, any, any, any, any, any>>(
76
+ parsed: P
77
+ ): ComputedLikeC4ModelFromParsed<P> {
78
+ const _computeView = mkComputeView(parsed)
79
+ const computeView = (source: LikeC4View): ComputedView => {
80
+ const result = _computeView(source)
81
+ if (result.isSuccess) {
82
+ return result.view
83
+ } else {
84
+ throw result.error
85
+ }
86
+ }
87
+ return {
88
+ __: 'computed',
89
+ ...parsed,
90
+ views: mapValues(parsed.views, computeView)
91
+ } as any // TODO: fix this
92
+ }
@@ -0,0 +1,108 @@
1
+ import type { DeploymentConnectionModel } from '../../model/connection/deployment'
2
+ import { difference } from '../../utils/set'
3
+ import type { Connections, Elem } from './_types'
4
+
5
+ export interface Memory {
6
+ /**
7
+ * All resolved elements, includes:
8
+ * - explicit elements (added directly, always appear in the view unless excluded)
9
+ * - elements from resolved connections (may be excluded, if connection is redundant @see excludeRedundantRelationships
10
+ * - implicit elements (not added directly, not included in the view, used for resolving connections)
11
+ */
12
+ readonly elements: ReadonlySet<Elem>
13
+ /**
14
+ * Resolved connections
15
+ * May contains duplicates, need to be merged before further processing
16
+ */
17
+ readonly connections: Connections
18
+
19
+ has(el: Elem): boolean
20
+
21
+ isExplicit(el: Elem): boolean
22
+
23
+ clone(): MutableMemory
24
+ }
25
+
26
+ export type Patch = (memory: Memory) => Memory
27
+
28
+ export class MutableMemory implements Memory {
29
+ static empty(): Memory {
30
+ return new MutableMemory(new Set(), new Set(), [], new Set())
31
+ }
32
+ constructor(
33
+ public elements: Set<Elem>,
34
+ public explicits: Set<Elem>,
35
+ public connections: DeploymentConnectionModel[],
36
+ /**
37
+ * Final set of elements to be included in the view
38
+ * (`elements` excluding implicits)
39
+ * Keeps order in which elements were added
40
+ */
41
+ public finalElements: Set<Elem>
42
+ ) {
43
+ }
44
+
45
+ public has(el: Elem): boolean {
46
+ return this.elements.has(el)
47
+ }
48
+
49
+ public isExplicit(el: Elem): boolean {
50
+ return this.explicits.has(el)
51
+ }
52
+
53
+ public exclude(excluded: Set<Elem>): MutableMemory {
54
+ const newMemory = this.clone()
55
+ newMemory.elements = difference(this.elements, excluded)
56
+ newMemory.explicits = difference(this.explicits, excluded)
57
+ newMemory.finalElements = difference(this.finalElements, excluded)
58
+ newMemory.connections = this.connections.filter(c => !excluded.has(c.source) && !excluded.has(c.target))
59
+ return newMemory
60
+ }
61
+
62
+ public excludeConnections(excluded: Connections): MutableMemory {
63
+ if (excluded.length === 0) {
64
+ return this
65
+ }
66
+ const newMemory = this.clone()
67
+
68
+ const excludedMap = new Map(excluded.map(c => [c.id, c]))
69
+ let minusElements = new Set<Elem>()
70
+
71
+ newMemory.connections = newMemory.connections.reduce((acc, c) => {
72
+ const excluded = excludedMap.get(c.id)
73
+ if (excluded) {
74
+ minusElements.add(c.source)
75
+ minusElements.add(c.target)
76
+ const diff = c.difference(excluded)
77
+ if (diff.nonEmpty()) {
78
+ acc.push(diff)
79
+ }
80
+ } else {
81
+ acc.push(c)
82
+ }
83
+ return acc
84
+ }, [] as DeploymentConnectionModel[])
85
+
86
+ minusElements = difference(minusElements, newMemory.explicits)
87
+ if (minusElements.size > 0) {
88
+ for (const stillExists of newMemory.connections) {
89
+ minusElements.delete(stillExists.source)
90
+ minusElements.delete(stillExists.target)
91
+ }
92
+ }
93
+ if (minusElements.size > 0) {
94
+ newMemory.elements = difference(newMemory.elements, minusElements)
95
+ newMemory.finalElements = difference(newMemory.finalElements, minusElements)
96
+ }
97
+ return newMemory
98
+ }
99
+
100
+ public clone(): MutableMemory {
101
+ return new MutableMemory(
102
+ new Set(this.elements),
103
+ new Set(this.explicits),
104
+ [...this.connections],
105
+ new Set(this.finalElements)
106
+ )
107
+ }
108
+ }
@@ -0,0 +1,145 @@
1
+ import { isArray, partition } from 'remeda'
2
+ import { mergeConnections } from '../../model/connection/deployment'
3
+ import type { DeploymentConnectionModel } from '../../model/connection/DeploymentConnectionModel'
4
+ import { union } from '../../utils/set'
5
+ import type { Connections, Elem } from './_types'
6
+ import { type Patch } from './Memory'
7
+
8
+ const asArray = <T>(value: T | ReadonlyArray<T>): ReadonlyArray<T> => isArray(value) ? value : [value]
9
+
10
+ /**
11
+ * Stage is a single step in the computation of the view
12
+ */
13
+ export class Stage {
14
+ // New elements
15
+ readonly #explicits = new Set<Elem>()
16
+ readonly #implicits = new Set<Elem>()
17
+
18
+ // Removed elements
19
+ readonly #excluded = new Set<Elem>()
20
+ readonly #excludedConnections = [] as DeploymentConnectionModel[]
21
+
22
+ #connections = [] as DeploymentConnectionModel[]
23
+
24
+ constructor(
25
+ public readonly previous: Stage | null = null
26
+ ) {}
27
+
28
+ get connections(): ReadonlyArray<DeploymentConnectionModel> {
29
+ return this.#connections
30
+ }
31
+
32
+ public addExplicit(elements: Elem | ReadonlyArray<Elem>): void {
33
+ for (const el of asArray(elements)) {
34
+ this.#explicits.add(el)
35
+ this.#implicits.delete(el)
36
+ }
37
+ }
38
+
39
+ public addImplicit(elements: Elem | ReadonlyArray<Elem>): void {
40
+ for (const el of asArray(elements)) {
41
+ if (this.#explicits.has(el)) {
42
+ continue
43
+ }
44
+ this.#implicits.add(el)
45
+ }
46
+ }
47
+
48
+ public addConnections(connections: Connections): Connections {
49
+ for (const c of connections) {
50
+ this.#connections.push(c)
51
+ this.addImplicit([c.source, c.target])
52
+ }
53
+ return connections
54
+ }
55
+
56
+ public exclude(elements: Elem | ReadonlyArray<Elem>): void {
57
+ if (this.#explicits.size + this.#implicits.size > 0) {
58
+ console.warn('Excluding elements from the stage with existing elements')
59
+ }
60
+ for (const el of asArray(elements)) {
61
+ this.#excluded.add(el)
62
+ this.#explicits.delete(el)
63
+ this.#implicits.delete(el)
64
+ }
65
+ if (this.#connections.length > 0) {
66
+ console.warn('Excluding elements from the stage with existing connections')
67
+ }
68
+ }
69
+
70
+ public excludeConnections(connections: Connections): void {
71
+ if (this.#connections.length > 0) {
72
+ console.warn('Excluding connections from the stage with existing connections')
73
+ }
74
+ for (const c of connections) {
75
+ this.#excludedConnections.push(c)
76
+ this.#implicits.delete(c.source)
77
+ this.#implicits.delete(c.target)
78
+ }
79
+ }
80
+
81
+ public hasConnections(): boolean {
82
+ return this.#connections.length > 0
83
+ }
84
+
85
+ /**
86
+ * Returns patch to apply to the memory
87
+ */
88
+ public patch(): Patch {
89
+ return (memory) => {
90
+ let newMemory = memory.clone()
91
+
92
+ let fromConnections = new Set<Elem>()
93
+
94
+ if (this.#connections.length > 0) {
95
+ // To preserve order, we split new connections into two sets
96
+ // First are outgoing from known elements (in memory.elements)
97
+ const [fromKnown, rest] = partition(
98
+ this.#connections,
99
+ c => memory.elements.has(c.source)
100
+ )
101
+
102
+ newMemory.connections = mergeConnections([
103
+ ...memory.connections,
104
+ ...fromKnown,
105
+ ...rest
106
+ ])
107
+
108
+ fromConnections = new Set([
109
+ // we have source in memory.elements
110
+ ...fromKnown.map(c => c.target),
111
+ ...rest.flatMap(c => [c.source, c.target])
112
+ ])
113
+ }
114
+
115
+ if (this.#explicits.size > 0 || fromConnections.size > 0 || this.#implicits.size > 0) {
116
+ newMemory.explicits = union(
117
+ newMemory.explicits,
118
+ this.#explicits
119
+ )
120
+
121
+ newMemory.finalElements = union(
122
+ newMemory.finalElements,
123
+ this.#explicits,
124
+ fromConnections
125
+ )
126
+
127
+ newMemory.elements = union(
128
+ newMemory.elements,
129
+ this.#explicits,
130
+ fromConnections,
131
+ this.#implicits
132
+ )
133
+ }
134
+
135
+ if (this.#excluded.size > 0) {
136
+ newMemory = newMemory.exclude(this.#excluded)
137
+ }
138
+ if (this.#excludedConnections.length > 0) {
139
+ newMemory = newMemory.excludeConnections(this.#excludedConnections)
140
+ }
141
+
142
+ return newMemory
143
+ }
144
+ }
145
+ }