@highstate/backend 0.18.0 → 0.20.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.
- package/dist/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
- package/dist/chunk-52MY2TCE.js.map +1 -0
- package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
- package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +4159 -785
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +5 -2
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +2 -2
- package/package.json +7 -7
- package/prisma/backend/_schema/object.prisma +12 -0
- package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
- package/prisma/project/artifact.prisma +3 -0
- package/prisma/project/entity.prisma +125 -0
- package/prisma/project/instance.prisma +6 -0
- package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
- package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
- package/prisma/project/operation.prisma +3 -0
- package/src/business/artifact.test.ts +22 -2
- package/src/business/artifact.ts +7 -1
- package/src/business/entity-snapshot.test.ts +684 -0
- package/src/business/entity-snapshot.ts +904 -0
- package/src/business/evaluation.test.ts +56 -0
- package/src/business/evaluation.ts +102 -22
- package/src/business/global-search.test.ts +344 -0
- package/src/business/global-search.ts +902 -0
- package/src/business/index.ts +4 -0
- package/src/business/instance-lock.ts +58 -74
- package/src/business/instance-state.test.ts +15 -1
- package/src/business/instance-state.ts +37 -14
- package/src/business/object-ref-index.test.ts +140 -0
- package/src/business/object-ref-index.ts +193 -0
- package/src/business/operation.test.ts +15 -1
- package/src/business/operation.ts +4 -0
- package/src/business/project-model.ts +154 -13
- package/src/business/project-unlock.ts +25 -2
- package/src/business/project.ts +9 -0
- package/src/business/secret.test.ts +35 -2
- package/src/business/secret.ts +32 -9
- package/src/business/settings.ts +761 -0
- package/src/business/unit-output.test.ts +477 -0
- package/src/business/unit-output.ts +461 -0
- package/src/business/worker.ts +55 -4
- package/src/database/_generated/backend/postgresql/browser.ts +6 -0
- package/src/database/_generated/backend/postgresql/client.ts +6 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
- package/src/database/_generated/backend/postgresql/models.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +6 -0
- package/src/database/_generated/backend/sqlite/client.ts +6 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
- package/src/database/_generated/backend/sqlite/models.ts +1 -0
- package/src/database/_generated/project/browser.ts +23 -0
- package/src/database/_generated/project/client.ts +23 -0
- package/src/database/_generated/project/commonInputTypes.ts +87 -53
- package/src/database/_generated/project/enums.ts +8 -0
- package/src/database/_generated/project/internal/class.ts +53 -5
- package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
- package/src/database/_generated/project/models/Artifact.ts +199 -11
- package/src/database/_generated/project/models/Entity.ts +1274 -0
- package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
- package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
- package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
- package/src/database/_generated/project/models/InstanceState.ts +361 -1
- package/src/database/_generated/project/models/Operation.ts +148 -3
- package/src/database/_generated/project/models/OperationLog.ts +0 -4
- package/src/database/_generated/project/models.ts +4 -0
- package/src/database/migration.ts +3 -0
- package/src/library/worker/evaluator.ts +7 -1
- package/src/orchestrator/manager.ts +7 -0
- package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
- package/src/orchestrator/operation-context.ts +154 -16
- package/src/orchestrator/operation-plan.destroy.test.md +33 -12
- package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
- package/src/orchestrator/operation-plan.fixtures.ts +2 -0
- package/src/orchestrator/operation-plan.md +4 -1
- package/src/orchestrator/operation-plan.ts +286 -92
- package/src/orchestrator/operation-plan.update.test.md +286 -11
- package/src/orchestrator/operation-plan.update.test.ts +656 -5
- package/src/orchestrator/operation-workset.ts +72 -22
- package/src/orchestrator/operation.cancel.test.ts +4 -0
- package/src/orchestrator/operation.composite.test.ts +341 -0
- package/src/orchestrator/operation.destroy.test.ts +4 -0
- package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
- package/src/orchestrator/operation.preview.test.ts +4 -0
- package/src/orchestrator/operation.refresh.test.ts +4 -0
- package/src/orchestrator/operation.test-utils.ts +52 -13
- package/src/orchestrator/operation.ts +228 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +110 -0
- package/src/orchestrator/operation.update.test.ts +4 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/orchestrator/unit-input-values.test.ts +450 -0
- package/src/orchestrator/unit-input-values.ts +281 -0
- package/src/pubsub/manager.ts +3 -0
- package/src/runner/abstractions.ts +23 -54
- package/src/runner/local.ts +109 -85
- package/src/services.ts +52 -1
- package/src/shared/models/prisma.ts +1 -0
- package/src/shared/models/project/entity.ts +121 -0
- package/src/shared/models/project/index.ts +1 -0
- package/src/shared/models/project/operation.ts +61 -3
- package/src/shared/models/project/state.ts +10 -0
- package/src/shared/models/project/worker.ts +7 -0
- package/src/shared/resolvers/effective-output-type.test.ts +494 -0
- package/src/shared/resolvers/effective-output-type.ts +162 -0
- package/src/shared/resolvers/index.ts +1 -0
- package/src/shared/resolvers/input.ts +61 -9
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/stable-json.ts +41 -0
- package/src/terminal/manager.ts +6 -0
- package/src/worker/manager.ts +97 -1
- package/dist/chunk-JT4KWE3B.js.map +0 -1
|
@@ -35,12 +35,42 @@ export function createOperationPlan(
|
|
|
35
35
|
requestedInstanceIds: string[],
|
|
36
36
|
options: OperationOptions,
|
|
37
37
|
): OperationPhase[] {
|
|
38
|
+
const enabledGhostStrategies = [
|
|
39
|
+
options.onlyDestroyGhosts,
|
|
40
|
+
options.firstDestroyGhosts,
|
|
41
|
+
options.ignoreGhosts,
|
|
42
|
+
].filter(Boolean).length
|
|
43
|
+
|
|
44
|
+
if (enabledGhostStrategies > 1) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Operation options are invalid: only one of onlyDestroyGhosts, firstDestroyGhosts, ignoreGhosts can be enabled.",
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (type !== "update" && enabledGhostStrategies > 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Operation options are invalid: onlyDestroyGhosts, firstDestroyGhosts and ignoreGhosts are supported only for update operations.",
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.forceUpdateDependencies && options.ignoreChangedDependencies) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Operation options are invalid: forceUpdateDependencies and ignoreChangedDependencies cannot both be enabled.",
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
if (options.forceUpdateDependencies && options.ignoreDependencies) {
|
|
39
63
|
throw new Error(
|
|
40
64
|
"Operation options are invalid: forceUpdateDependencies and ignoreDependencies cannot both be enabled.",
|
|
41
65
|
)
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
if (options.ignoreChangedDependencies && options.ignoreDependencies) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Operation options are invalid: ignoreChangedDependencies and ignoreDependencies cannot both be enabled.",
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
if (type === "preview") {
|
|
45
75
|
if (requestedInstanceIds.length !== 1) {
|
|
46
76
|
throw new Error("Preview operations can only target a single instance")
|
|
@@ -123,7 +153,14 @@ function processInstance(
|
|
|
123
153
|
options: OperationOptions,
|
|
124
154
|
operationType: OperationType,
|
|
125
155
|
): void {
|
|
126
|
-
const instance =
|
|
156
|
+
const instance =
|
|
157
|
+
operationType === "destroy"
|
|
158
|
+
? context.tryGetInstanceForDestroy(instanceId)
|
|
159
|
+
: context.tryGetInstance(instanceId)
|
|
160
|
+
|
|
161
|
+
if (!instance) {
|
|
162
|
+
throw new Error(`Instance with ID ${instanceId} not found in the operation context`)
|
|
163
|
+
}
|
|
127
164
|
|
|
128
165
|
// update composite classification
|
|
129
166
|
updateCompositeClassification(instance, workState, context)
|
|
@@ -140,7 +177,7 @@ function processInstance(
|
|
|
140
177
|
}
|
|
141
178
|
|
|
142
179
|
// propagate to related instances
|
|
143
|
-
propagateToRelated(instance, workState)
|
|
180
|
+
propagateToRelated(instance, workState, context)
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
function updateCompositeClassification(
|
|
@@ -171,7 +208,11 @@ function updateCompositeClassification(
|
|
|
171
208
|
// check if this dependency is external to this composite
|
|
172
209
|
const requiredBy = workState.dependencyRequiredBy.get(child.id)
|
|
173
210
|
if (requiredBy) {
|
|
174
|
-
const requiredByInstance = context.
|
|
211
|
+
const requiredByInstance = context.tryGetInstance(requiredBy)
|
|
212
|
+
if (!requiredByInstance) {
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
// if the requiring instance is not a child of this composite, it's external
|
|
176
217
|
if (requiredByInstance.parentId !== instance.id) {
|
|
177
218
|
hasExternalDependencyChildren = true
|
|
@@ -234,7 +275,12 @@ function processUpdateInclusions(
|
|
|
234
275
|
continue
|
|
235
276
|
}
|
|
236
277
|
|
|
237
|
-
const
|
|
278
|
+
const dependencyState = context.getState(depInstance.id)
|
|
279
|
+
|
|
280
|
+
const shouldInclude =
|
|
281
|
+
options.forceUpdateDependencies ||
|
|
282
|
+
(!options.ignoreChangedDependencies && isOutdated(depInstance, context)) ||
|
|
283
|
+
isFailedOrUndeployedState(dependencyState)
|
|
238
284
|
|
|
239
285
|
if (shouldInclude && !workState.included.has(depInstance.id)) {
|
|
240
286
|
include(depInstance.id, "dependency", workState, {
|
|
@@ -276,7 +322,7 @@ function processRefreshInclusions(
|
|
|
276
322
|
if (workState.included.has(instance.id)) {
|
|
277
323
|
const dependencies = context.getDependencies(instance.id)
|
|
278
324
|
for (const depInstance of dependencies) {
|
|
279
|
-
if (options.ignoreDependencies) {
|
|
325
|
+
if (options.ignoreDependencies || options.ignoreChangedDependencies) {
|
|
280
326
|
continue
|
|
281
327
|
}
|
|
282
328
|
|
|
@@ -292,6 +338,10 @@ function processRefreshInclusions(
|
|
|
292
338
|
}
|
|
293
339
|
}
|
|
294
340
|
|
|
341
|
+
function isFailedOrUndeployedState(state: InstanceState): boolean {
|
|
342
|
+
return state.status === "failed" || state.status === "undeployed"
|
|
343
|
+
}
|
|
344
|
+
|
|
295
345
|
function processDestroyInclusions(
|
|
296
346
|
instance: InstanceModel,
|
|
297
347
|
workState: WorkState,
|
|
@@ -324,21 +374,16 @@ function processDestroyInclusions(
|
|
|
324
374
|
}
|
|
325
375
|
}
|
|
326
376
|
|
|
327
|
-
function propagateToRelated(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (inclusionReason === "parent_composite") {
|
|
333
|
-
// compositional boundary - don't propagate upward
|
|
334
|
-
return
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
377
|
+
function propagateToRelated(
|
|
378
|
+
instance: InstanceModel,
|
|
379
|
+
workState: WorkState,
|
|
380
|
+
context: OperationContext,
|
|
381
|
+
): void {
|
|
338
382
|
// propagate upward to parent if instance is included
|
|
339
383
|
if (
|
|
340
384
|
workState.included.has(instance.id) &&
|
|
341
385
|
instance.parentId &&
|
|
386
|
+
context.tryGetInstance(instance.parentId) &&
|
|
342
387
|
!workState.included.has(instance.parentId)
|
|
343
388
|
) {
|
|
344
389
|
include(instance.parentId, "parent_composite", workState, {
|
|
@@ -361,7 +406,11 @@ function findSubstantiveAncestor(
|
|
|
361
406
|
return currentId
|
|
362
407
|
}
|
|
363
408
|
|
|
364
|
-
const instance = context.
|
|
409
|
+
const instance = context.tryGetInstance(currentId)
|
|
410
|
+
if (!instance) {
|
|
411
|
+
return null
|
|
412
|
+
}
|
|
413
|
+
|
|
365
414
|
currentId = instance.parentId
|
|
366
415
|
}
|
|
367
416
|
|
|
@@ -436,11 +485,7 @@ function createOrderedPhases(
|
|
|
436
485
|
// include parent composites only if they have children needing updates
|
|
437
486
|
if (inclusionReason === "parent_composite") {
|
|
438
487
|
const children = context.getInstanceChildren(id)
|
|
439
|
-
return children.some(
|
|
440
|
-
child =>
|
|
441
|
-
workState.included.has(child.id) &&
|
|
442
|
-
workState.included.get(child.id) !== "parent_composite",
|
|
443
|
-
)
|
|
488
|
+
return children.some(child => workState.included.has(child.id))
|
|
444
489
|
}
|
|
445
490
|
|
|
446
491
|
// include other types (dependency, composite_child, etc.)
|
|
@@ -448,68 +493,50 @@ function createOrderedPhases(
|
|
|
448
493
|
})
|
|
449
494
|
|
|
450
495
|
const updateInstances = topologicalSort(instancesNeedingUpdate, context, false)
|
|
451
|
-
.map(id => createPhaseInstance(id, context, workState))
|
|
496
|
+
.map(id => createPhaseInstance(id, context, type, workState))
|
|
452
497
|
.filter(inst => inst !== null) as OperationPhaseInstance[]
|
|
453
498
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
499
|
+
const phaseType = type === "refresh" ? "refresh" : "update"
|
|
500
|
+
const updatePhase =
|
|
501
|
+
updateInstances.length > 0 ? ({ type: phaseType, instances: updateInstances } as const) : null
|
|
458
502
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const instance = context.getInstance(instanceId)
|
|
464
|
-
if (instance.kind !== "composite") continue
|
|
503
|
+
const ghostDestroyPhase =
|
|
504
|
+
type !== "refresh" && !options.ignoreGhosts
|
|
505
|
+
? createGhostDestroyPhase(context, includedIds, workState)
|
|
506
|
+
: null
|
|
465
507
|
|
|
466
|
-
|
|
467
|
-
|
|
508
|
+
if (type === "update" && options.onlyDestroyGhosts) {
|
|
509
|
+
if (ghostDestroyPhase) {
|
|
510
|
+
phases.push(ghostDestroyPhase)
|
|
511
|
+
}
|
|
468
512
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const hasGhostChildren = children.some(child => {
|
|
472
|
-
const state = context.getState(child.id)
|
|
473
|
-
return isVirtualGhostInstance(state)
|
|
474
|
-
})
|
|
513
|
+
return phases
|
|
514
|
+
}
|
|
475
515
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
516
|
+
if (type === "update" && options.firstDestroyGhosts) {
|
|
517
|
+
if (ghostDestroyPhase) {
|
|
518
|
+
phases.push(ghostDestroyPhase)
|
|
479
519
|
}
|
|
480
|
-
const ghostInstances = findGhostCleanup(context, compositesNeedingGhostCleanup)
|
|
481
|
-
|
|
482
|
-
if (ghostInstances.length > 0) {
|
|
483
|
-
const ghostInstanceMap = new Map<InstanceId, OperationPhaseInstance>(
|
|
484
|
-
ghostInstances.map(instance => [instance.id, instance]),
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
const sortedGhosts = topologicalSort(
|
|
488
|
-
ghostInstances.map(g => g.id),
|
|
489
|
-
context,
|
|
490
|
-
true,
|
|
491
|
-
)
|
|
492
|
-
.map(id => {
|
|
493
|
-
const ghostInstance = ghostInstanceMap.get(id)
|
|
494
|
-
|
|
495
|
-
if (ghostInstance?.message === "ghost cleanup") {
|
|
496
|
-
return ghostInstance
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return createPhaseInstance(id, context, workState)
|
|
500
|
-
})
|
|
501
|
-
.filter((instance): instance is OperationPhaseInstance => instance !== null)
|
|
502
520
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
521
|
+
if (updatePhase) {
|
|
522
|
+
phases.push(updatePhase)
|
|
506
523
|
}
|
|
524
|
+
|
|
525
|
+
return phases
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (updatePhase) {
|
|
529
|
+
phases.push(updatePhase)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (ghostDestroyPhase) {
|
|
533
|
+
phases.push(ghostDestroyPhase)
|
|
507
534
|
}
|
|
508
535
|
}
|
|
509
536
|
|
|
510
537
|
if (type === "destroy") {
|
|
511
538
|
const destroyInstances = topologicalSort(includedIds, context, true)
|
|
512
|
-
.map(id => createPhaseInstance(id, context, workState))
|
|
539
|
+
.map(id => createPhaseInstance(id, context, type, workState))
|
|
513
540
|
.filter(inst => inst !== null) as OperationPhaseInstance[]
|
|
514
541
|
|
|
515
542
|
if (destroyInstances.length > 0) {
|
|
@@ -519,11 +546,11 @@ function createOrderedPhases(
|
|
|
519
546
|
|
|
520
547
|
if (type === "recreate") {
|
|
521
548
|
const destroyInstances = topologicalSort(includedIds, context, true)
|
|
522
|
-
.map(id => createPhaseInstance(id, context, workState))
|
|
549
|
+
.map(id => createPhaseInstance(id, context, type, workState))
|
|
523
550
|
.filter(inst => inst !== null) as OperationPhaseInstance[]
|
|
524
551
|
|
|
525
552
|
const updateInstances = topologicalSort(includedIds, context, false)
|
|
526
|
-
.map(id => createPhaseInstance(id, context, workState))
|
|
553
|
+
.map(id => createPhaseInstance(id, context, type, workState))
|
|
527
554
|
.filter(inst => inst !== null) as OperationPhaseInstance[]
|
|
528
555
|
|
|
529
556
|
if (destroyInstances.length > 0) {
|
|
@@ -537,12 +564,101 @@ function createOrderedPhases(
|
|
|
537
564
|
return phases
|
|
538
565
|
}
|
|
539
566
|
|
|
567
|
+
function createGhostDestroyPhase(
|
|
568
|
+
context: OperationContext,
|
|
569
|
+
includedIds: InstanceId[],
|
|
570
|
+
workState: WorkState,
|
|
571
|
+
): OperationPhase | null {
|
|
572
|
+
const compositesNeedingGhostCleanup = new Set<InstanceId>()
|
|
573
|
+
|
|
574
|
+
for (const instanceId of includedIds) {
|
|
575
|
+
const instance = context.getInstance(instanceId)
|
|
576
|
+
if (instance.kind !== "composite") continue
|
|
577
|
+
|
|
578
|
+
const compositeType = workState.compositeTypes.get(instanceId)
|
|
579
|
+
if (compositeType !== "substantive") continue
|
|
580
|
+
|
|
581
|
+
if (hasGhostDescendant(instanceId, context)) {
|
|
582
|
+
compositesNeedingGhostCleanup.add(instanceId)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const ghostInstances = findGhostCleanup(context, compositesNeedingGhostCleanup)
|
|
587
|
+
if (ghostInstances.length === 0) {
|
|
588
|
+
return null
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const ghostInstanceMap = new Map<InstanceId, OperationPhaseInstance>(
|
|
592
|
+
ghostInstances.map(instance => [instance.id, instance]),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
const sortedGhosts = topologicalSort(
|
|
596
|
+
ghostInstances.map(g => g.id),
|
|
597
|
+
context,
|
|
598
|
+
true,
|
|
599
|
+
)
|
|
600
|
+
.map(id => {
|
|
601
|
+
const ghostInstance = ghostInstanceMap.get(id)
|
|
602
|
+
|
|
603
|
+
if (ghostInstance?.message === "ghost cleanup") {
|
|
604
|
+
return ghostInstance
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return createPhaseInstance(id, context, "update", workState)
|
|
608
|
+
})
|
|
609
|
+
.filter((instance): instance is OperationPhaseInstance => instance !== null)
|
|
610
|
+
|
|
611
|
+
if (sortedGhosts.length === 0) {
|
|
612
|
+
return null
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return { type: "destroy", instances: sortedGhosts }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function hasGhostDescendant(instanceId: InstanceId, context: OperationContext): boolean {
|
|
619
|
+
const queue = context.getInstanceChildren(instanceId).map(child => child.id)
|
|
620
|
+
|
|
621
|
+
while (queue.length > 0) {
|
|
622
|
+
const childId = queue.shift()!
|
|
623
|
+
const child = context.getInstance(childId)
|
|
624
|
+
const childState = context.getState(child.id)
|
|
625
|
+
|
|
626
|
+
if (isVirtualGhostInstance(childState)) {
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (child.kind === "composite") {
|
|
631
|
+
queue.push(...context.getInstanceChildren(child.id).map(instance => instance.id))
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
|
|
540
638
|
function createPhaseInstance(
|
|
541
639
|
instanceId: InstanceId,
|
|
542
640
|
context: OperationContext,
|
|
641
|
+
operationType: OperationType,
|
|
543
642
|
workState?: WorkState,
|
|
544
643
|
): OperationPhaseInstance | null {
|
|
545
|
-
const
|
|
644
|
+
const state = context.getState(instanceId)
|
|
645
|
+
const instance =
|
|
646
|
+
operationType === "destroy"
|
|
647
|
+
? context.tryGetInstanceForDestroy(instanceId)
|
|
648
|
+
: context.tryGetInstance(instanceId)
|
|
649
|
+
|
|
650
|
+
if (!instance) {
|
|
651
|
+
return null
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (
|
|
655
|
+
operationType !== "destroy" &&
|
|
656
|
+
instance.kind === "composite" &&
|
|
657
|
+
!hasUnitDescendant(instanceId, context)
|
|
658
|
+
) {
|
|
659
|
+
return null
|
|
660
|
+
}
|
|
661
|
+
|
|
546
662
|
let message = "included in operation" // fallback
|
|
547
663
|
|
|
548
664
|
if (workState) {
|
|
@@ -550,7 +666,7 @@ function createPhaseInstance(
|
|
|
550
666
|
const requiredBy = workState.dependencyRequiredBy.get(instanceId)
|
|
551
667
|
const triggeringChild = workState.childTriggeringParent.get(instanceId)
|
|
552
668
|
const forceFlag = workState.forceFlags.get(instanceId)
|
|
553
|
-
const instanceState =
|
|
669
|
+
const instanceState = state
|
|
554
670
|
|
|
555
671
|
message = generateContextualMessage(
|
|
556
672
|
context,
|
|
@@ -565,11 +681,34 @@ function createPhaseInstance(
|
|
|
565
681
|
|
|
566
682
|
return {
|
|
567
683
|
id: instanceId,
|
|
568
|
-
parentId: instance.parentId,
|
|
684
|
+
parentId: instance.parentId ?? state.parentInstanceId ?? undefined,
|
|
569
685
|
message,
|
|
570
686
|
}
|
|
571
687
|
}
|
|
572
688
|
|
|
689
|
+
function hasUnitDescendant(instanceId: InstanceId, context: OperationContext): boolean {
|
|
690
|
+
const queue = context.getInstanceChildren(instanceId).map(child => child.id)
|
|
691
|
+
const visited = new Set<InstanceId>()
|
|
692
|
+
|
|
693
|
+
while (queue.length > 0) {
|
|
694
|
+
const childId = queue.pop()!
|
|
695
|
+
|
|
696
|
+
if (visited.has(childId)) {
|
|
697
|
+
continue
|
|
698
|
+
}
|
|
699
|
+
visited.add(childId)
|
|
700
|
+
|
|
701
|
+
const child = context.getInstance(childId)
|
|
702
|
+
if (child.kind === "unit") {
|
|
703
|
+
return true
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
queue.push(...context.getInstanceChildren(childId).map(instance => instance.id))
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return false
|
|
710
|
+
}
|
|
711
|
+
|
|
573
712
|
function generateContextualMessage(
|
|
574
713
|
context: OperationContext,
|
|
575
714
|
instanceId: InstanceId,
|
|
@@ -586,7 +725,10 @@ function generateContextualMessage(
|
|
|
586
725
|
if (state.status === "failed") return "failed"
|
|
587
726
|
if (state.status === "undeployed") return "undeployed"
|
|
588
727
|
|
|
589
|
-
const instance = context.
|
|
728
|
+
const instance = context.tryGetInstance(instanceId)
|
|
729
|
+
if (!instance) {
|
|
730
|
+
return "up-to-date"
|
|
731
|
+
}
|
|
590
732
|
|
|
591
733
|
// composites are containers and cannot be changed/outdated
|
|
592
734
|
if (instance.kind === "composite") {
|
|
@@ -654,32 +796,84 @@ function findGhostCleanup(
|
|
|
654
796
|
context: OperationContext,
|
|
655
797
|
compositesNeedingGhostCleanup: Set<InstanceId>,
|
|
656
798
|
): OperationPhaseInstance[] {
|
|
657
|
-
const
|
|
799
|
+
const requiredCompositeIds = new Set<InstanceId>()
|
|
800
|
+
const ghostIds = new Set<InstanceId>()
|
|
801
|
+
|
|
802
|
+
function collectGhostsInSubtree(rootCompositeId: InstanceId): void {
|
|
803
|
+
const queue: InstanceId[] = [rootCompositeId]
|
|
804
|
+
|
|
805
|
+
while (queue.length > 0) {
|
|
806
|
+
const currentId = queue.shift()!
|
|
807
|
+
const children = context.getInstanceChildren(currentId)
|
|
808
|
+
|
|
809
|
+
for (const child of children) {
|
|
810
|
+
const childState = context.getState(child.id)
|
|
811
|
+
|
|
812
|
+
if (child.kind === "composite") {
|
|
813
|
+
queue.push(child.id)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (!isVirtualGhostInstance(childState)) {
|
|
817
|
+
continue
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
ghostIds.add(child.id)
|
|
821
|
+
|
|
822
|
+
// Include the full composite chain from the ghost parent up to the cleanup root,
|
|
823
|
+
// otherwise destroy-phase parent state recalculation may target non-participating composites.
|
|
824
|
+
let parentId = child.parentId
|
|
825
|
+
while (parentId) {
|
|
826
|
+
const parent = context.tryGetInstance(parentId)
|
|
827
|
+
if (!parent || parent.kind !== "composite") {
|
|
828
|
+
break
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
requiredCompositeIds.add(parentId)
|
|
832
|
+
|
|
833
|
+
if (parentId === rootCompositeId) {
|
|
834
|
+
break
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
parentId = parent.parentId
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
658
842
|
|
|
659
|
-
// find ghost instances and their parent composites that need cleanup
|
|
660
843
|
for (const instanceId of compositesNeedingGhostCleanup) {
|
|
661
844
|
const instance = context.getInstance(instanceId)
|
|
662
845
|
if (instance.kind !== "composite") continue
|
|
663
846
|
|
|
664
|
-
|
|
847
|
+
requiredCompositeIds.add(instanceId)
|
|
848
|
+
collectGhostsInSubtree(instanceId)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const ghosts: OperationPhaseInstance[] = []
|
|
852
|
+
|
|
853
|
+
for (const compositeId of requiredCompositeIds) {
|
|
854
|
+
const composite = context.tryGetInstance(compositeId)
|
|
855
|
+
if (!composite) {
|
|
856
|
+
continue
|
|
857
|
+
}
|
|
858
|
+
|
|
665
859
|
ghosts.push({
|
|
666
|
-
id:
|
|
667
|
-
parentId:
|
|
860
|
+
id: compositeId,
|
|
861
|
+
parentId: composite.parentId,
|
|
668
862
|
message: "included in operation",
|
|
669
863
|
})
|
|
864
|
+
}
|
|
670
865
|
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (isVirtualGhostInstance(state)) {
|
|
676
|
-
ghosts.push({
|
|
677
|
-
id: child.id,
|
|
678
|
-
parentId: child.parentId,
|
|
679
|
-
message: "ghost cleanup",
|
|
680
|
-
})
|
|
681
|
-
}
|
|
866
|
+
for (const ghostId of ghostIds) {
|
|
867
|
+
const ghost = context.tryGetInstance(ghostId)
|
|
868
|
+
if (!ghost) {
|
|
869
|
+
continue
|
|
682
870
|
}
|
|
871
|
+
|
|
872
|
+
ghosts.push({
|
|
873
|
+
id: ghostId,
|
|
874
|
+
parentId: ghost.parentId,
|
|
875
|
+
message: "ghost cleanup",
|
|
876
|
+
})
|
|
683
877
|
}
|
|
684
878
|
|
|
685
879
|
return ghosts
|