@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.
Files changed (120) hide show
  1. package/dist/{chunk-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
  2. package/dist/chunk-52MY2TCE.js.map +1 -0
  3. package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
  4. package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
  5. package/dist/highstate.manifest.json +4 -4
  6. package/dist/index.js +4159 -785
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +5 -2
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +2 -2
  11. package/package.json +7 -7
  12. package/prisma/backend/_schema/object.prisma +12 -0
  13. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  14. package/prisma/project/artifact.prisma +3 -0
  15. package/prisma/project/entity.prisma +125 -0
  16. package/prisma/project/instance.prisma +6 -0
  17. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  18. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  19. package/prisma/project/operation.prisma +3 -0
  20. package/src/business/artifact.test.ts +22 -2
  21. package/src/business/artifact.ts +7 -1
  22. package/src/business/entity-snapshot.test.ts +684 -0
  23. package/src/business/entity-snapshot.ts +904 -0
  24. package/src/business/evaluation.test.ts +56 -0
  25. package/src/business/evaluation.ts +102 -22
  26. package/src/business/global-search.test.ts +344 -0
  27. package/src/business/global-search.ts +902 -0
  28. package/src/business/index.ts +4 -0
  29. package/src/business/instance-lock.ts +58 -74
  30. package/src/business/instance-state.test.ts +15 -1
  31. package/src/business/instance-state.ts +37 -14
  32. package/src/business/object-ref-index.test.ts +140 -0
  33. package/src/business/object-ref-index.ts +193 -0
  34. package/src/business/operation.test.ts +15 -1
  35. package/src/business/operation.ts +4 -0
  36. package/src/business/project-model.ts +154 -13
  37. package/src/business/project-unlock.ts +25 -2
  38. package/src/business/project.ts +9 -0
  39. package/src/business/secret.test.ts +35 -2
  40. package/src/business/secret.ts +32 -9
  41. package/src/business/settings.ts +761 -0
  42. package/src/business/unit-output.test.ts +477 -0
  43. package/src/business/unit-output.ts +461 -0
  44. package/src/business/worker.ts +55 -4
  45. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  47. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  49. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  50. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  51. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  52. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  54. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  56. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  57. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  58. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  59. package/src/database/_generated/project/browser.ts +23 -0
  60. package/src/database/_generated/project/client.ts +23 -0
  61. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  62. package/src/database/_generated/project/enums.ts +8 -0
  63. package/src/database/_generated/project/internal/class.ts +53 -5
  64. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  65. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  66. package/src/database/_generated/project/models/Artifact.ts +199 -11
  67. package/src/database/_generated/project/models/Entity.ts +1274 -0
  68. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  69. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  70. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  71. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  72. package/src/database/_generated/project/models/Operation.ts +148 -3
  73. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  74. package/src/database/_generated/project/models.ts +4 -0
  75. package/src/database/migration.ts +3 -0
  76. package/src/library/worker/evaluator.ts +7 -1
  77. package/src/orchestrator/manager.ts +7 -0
  78. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  79. package/src/orchestrator/operation-context.ts +154 -16
  80. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  81. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  82. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  83. package/src/orchestrator/operation-plan.md +4 -1
  84. package/src/orchestrator/operation-plan.ts +286 -92
  85. package/src/orchestrator/operation-plan.update.test.md +286 -11
  86. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  87. package/src/orchestrator/operation-workset.ts +72 -22
  88. package/src/orchestrator/operation.cancel.test.ts +4 -0
  89. package/src/orchestrator/operation.composite.test.ts +341 -0
  90. package/src/orchestrator/operation.destroy.test.ts +4 -0
  91. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  92. package/src/orchestrator/operation.preview.test.ts +4 -0
  93. package/src/orchestrator/operation.refresh.test.ts +4 -0
  94. package/src/orchestrator/operation.test-utils.ts +52 -13
  95. package/src/orchestrator/operation.ts +228 -68
  96. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  97. package/src/orchestrator/operation.update.skip.test.ts +110 -0
  98. package/src/orchestrator/operation.update.test.ts +4 -0
  99. package/src/orchestrator/plan-test-builder.ts +1 -0
  100. package/src/orchestrator/unit-input-values.test.ts +450 -0
  101. package/src/orchestrator/unit-input-values.ts +281 -0
  102. package/src/pubsub/manager.ts +3 -0
  103. package/src/runner/abstractions.ts +23 -54
  104. package/src/runner/local.ts +109 -85
  105. package/src/services.ts +52 -1
  106. package/src/shared/models/prisma.ts +1 -0
  107. package/src/shared/models/project/entity.ts +121 -0
  108. package/src/shared/models/project/index.ts +1 -0
  109. package/src/shared/models/project/operation.ts +61 -3
  110. package/src/shared/models/project/state.ts +10 -0
  111. package/src/shared/models/project/worker.ts +7 -0
  112. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  113. package/src/shared/resolvers/effective-output-type.ts +162 -0
  114. package/src/shared/resolvers/index.ts +1 -0
  115. package/src/shared/resolvers/input.ts +61 -9
  116. package/src/shared/utils/index.ts +1 -0
  117. package/src/shared/utils/stable-json.ts +41 -0
  118. package/src/terminal/manager.ts +6 -0
  119. package/src/worker/manager.ts +97 -1
  120. package/dist/chunk-JT4KWE3B.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import type { Logger } from "pino"
2
2
  import type { ArtifactService } from "../artifact"
3
3
  import type {
4
+ EntitySnapshotService,
4
5
  InstanceLockService,
5
6
  InstanceStatePatch,
6
7
  InstanceStateService,
@@ -8,6 +9,7 @@ import type {
8
9
  ProjectModelService,
9
10
  SecretService,
10
11
  UnitExtraService,
12
+ UnitOutputService,
11
13
  } from "../business"
12
14
  import type { Operation, OperationUpdateInput, Project } from "../database"
13
15
  import type { LibraryBackend } from "../library"
@@ -18,7 +20,7 @@ import {
18
20
  parseInstanceId,
19
21
  type TriggerInvocation,
20
22
  type UnitConfig,
21
- type UnitInputReference,
23
+ type UnitInputValue,
22
24
  type VersionedName,
23
25
  } from "@highstate/contract"
24
26
  import { createId } from "@paralleldrive/cuid2"
@@ -30,15 +32,18 @@ import {
30
32
  type OperationPhase,
31
33
  PromiseTracker,
32
34
  type ResolvedInstanceInput,
35
+ resolveEffectiveOutputType,
33
36
  waitAll,
34
37
  } from "../shared"
35
38
  import { OperationContext } from "./operation-context"
36
39
  import { createOperationPlan } from "./operation-plan"
37
40
  import { OperationWorkset } from "./operation-workset"
41
+ import { resolveUnitInputValues } from "./unit-input-values"
38
42
 
39
43
  export class RuntimeOperation {
40
44
  private readonly instancePromiseMap = new Map<InstanceId, Promise<void>>()
41
45
  private readonly promiseTracker = new PromiseTracker()
46
+ private readonly pendingGhostDeletionIds = new Set<InstanceId>()
42
47
 
43
48
  private workset!: OperationWorkset
44
49
  private context!: OperationContext
@@ -57,6 +62,8 @@ export class RuntimeOperation {
57
62
  private readonly instanceStateService: InstanceStateService,
58
63
  private readonly projectModelService: ProjectModelService,
59
64
  private readonly unitExtraService: UnitExtraService,
65
+ private readonly entitySnapshotService: EntitySnapshotService,
66
+ private readonly unitOutputService: UnitOutputService,
60
67
  private readonly logger: Logger,
61
68
  ) {}
62
69
 
@@ -92,17 +99,21 @@ export class RuntimeOperation {
92
99
  }
93
100
 
94
101
  this.logger.error({ error }, "an error occurred while running the operation")
102
+ console.error(error)
95
103
 
96
104
  await this.updateOperation({ status: "failed" })
97
105
  await this.writeOperationLog(errorToString(error))
98
106
  } finally {
99
107
  try {
108
+ this.promiseTracker.track(this.flushGhostInstanceDeletions())
100
109
  this.promiseTracker.track(this.ensureInstancesUnlocked())
101
110
  this.promiseTracker.track(this.ensureOperationStatesFinalized())
102
111
 
103
112
  // ensure that all promises are resolved even if the operation failed
104
113
  await this.promiseTracker.waitForAll()
105
114
  } catch (error) {
115
+ console.error(error)
116
+
106
117
  this.logger.error(
107
118
  { error },
108
119
  "one of the tracked promises failed after the operation failed",
@@ -111,6 +122,17 @@ export class RuntimeOperation {
111
122
  }
112
123
  }
113
124
 
125
+ private async flushGhostInstanceDeletions(): Promise<void> {
126
+ if (this.pendingGhostDeletionIds.size === 0) {
127
+ return
128
+ }
129
+
130
+ const instanceIds = Array.from(this.pendingGhostDeletionIds)
131
+ this.pendingGhostDeletionIds.clear()
132
+
133
+ this.instanceStateService.publishGhostInstanceDeletion(this.project.id, instanceIds)
134
+ }
135
+
114
136
  private async operate(): Promise<void> {
115
137
  this.logger.info("starting operation")
116
138
 
@@ -120,6 +142,7 @@ export class RuntimeOperation {
120
142
  this.libraryBackend,
121
143
  this.instanceStateService,
122
144
  this.projectModelService,
145
+ this.entitySnapshotService,
123
146
  this.logger,
124
147
  )
125
148
 
@@ -143,6 +166,7 @@ export class RuntimeOperation {
143
166
  this.workset = new OperationWorkset(
144
167
  this.project,
145
168
  this.operation.id,
169
+ this.operation.type,
146
170
  plan,
147
171
  this.context,
148
172
  this.instanceStateService,
@@ -205,7 +229,7 @@ export class RuntimeOperation {
205
229
 
206
230
  // lauch all instances in this phase
207
231
  for (const instanceId of this.workset.phaseAffectedInstanceIds) {
208
- const instance = this.context.getInstance(instanceId)
232
+ const instance = this.getPhaseInstance(instanceId)
209
233
  const state = this.context.getState(instanceId)
210
234
  const promise = this.getInstancePromiseForOperation(instance, state)
211
235
 
@@ -291,7 +315,7 @@ export class RuntimeOperation {
291
315
  const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
292
316
  signal.throwIfAborted()
293
317
 
294
- const config = this.prepareUnitConfig(instance, secrets)
318
+ const config = this.prepareUnitConfig(instance, state.id, secrets)
295
319
  const artifactIds = this.collectArtifactIdsForInstance(instance)
296
320
  const artifacts = await this.artifactService.getArtifactsByIds(this.project.id, artifactIds)
297
321
 
@@ -299,13 +323,13 @@ export class RuntimeOperation {
299
323
 
300
324
  await this.runnerBackend.preview({
301
325
  projectId: this.project.id,
326
+ operationId: this.operation.id,
302
327
  libraryId: this.project.libraryId,
303
328
  stateId: state.id,
304
329
  instanceType: instance.type,
305
330
  instanceName: instance.name,
306
331
  config,
307
332
  refresh: this.operation.options.refresh,
308
- secrets,
309
333
  artifacts,
310
334
  signal,
311
335
  forceSignal,
@@ -351,7 +375,7 @@ export class RuntimeOperation {
351
375
  for (const child of children) {
352
376
  logger.debug(`waiting for child "%s"`, child)
353
377
 
354
- const instance = this.context.getInstance(child)
378
+ const instance = this.getPhaseInstance(child)
355
379
  const state = this.context.getState(child)
356
380
  const promise = this.getInstancePromiseForOperation(instance, state)
357
381
 
@@ -406,7 +430,8 @@ export class RuntimeOperation {
406
430
  // ignore when side effects are requested
407
431
  !this.operation.options.refresh &&
408
432
  !this.operation.options.deleteUnreachableResources &&
409
- !this.operation.options.forceUpdateDependencies
433
+ !this.operation.options.forceUpdateDependencies &&
434
+ !this.operation.options.forceUpdateChildren
410
435
  ) {
411
436
  const expected = await this.context.getUpToDateInputHashOutput(instance)
412
437
 
@@ -426,6 +451,7 @@ export class RuntimeOperation {
426
451
  },
427
452
  instanceState: {
428
453
  inputHash: expected.inputHash,
454
+ parentId: instance.parentId ? this.context.getState(instance.parentId).id : null,
429
455
  },
430
456
  })
431
457
 
@@ -448,7 +474,7 @@ export class RuntimeOperation {
448
474
 
449
475
  signal.throwIfAborted()
450
476
 
451
- const config = this.prepareUnitConfig(instance, secrets)
477
+ const config = this.prepareUnitConfig(instance, state.id, secrets)
452
478
 
453
479
  // collect artifacts authorized for this instance
454
480
  const artifactIds = this.collectArtifactIdsForInstance(instance)
@@ -458,6 +484,7 @@ export class RuntimeOperation {
458
484
 
459
485
  await this.runnerBackend.update({
460
486
  projectId: this.project.id,
487
+ operationId: this.operation.id,
461
488
  libraryId: this.project.libraryId,
462
489
  stateId: state.id,
463
490
  instanceType: instance.type,
@@ -465,7 +492,6 @@ export class RuntimeOperation {
465
492
  config,
466
493
  refresh: this.operation.options.refresh,
467
494
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
468
- secrets,
469
495
  artifacts,
470
496
  signal,
471
497
  forceSignal,
@@ -541,14 +567,14 @@ export class RuntimeOperation {
541
567
 
542
568
  await this.runnerBackend.update({
543
569
  projectId: this.project.id,
570
+ operationId: this.operation.id,
544
571
  stateId: state.id,
545
572
  libraryId: this.project.libraryId,
546
573
  instanceType: instance.type,
547
574
  instanceName: instance.name,
548
- config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
575
+ config: this.prepareUnitConfig(instance, state.id, secrets, invokedTriggers),
549
576
  refresh: this.operation.options.refresh,
550
577
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
551
- secrets,
552
578
  signal,
553
579
  forceSignal,
554
580
  debug: this.operation.options.debug,
@@ -571,7 +597,7 @@ export class RuntimeOperation {
571
597
  continue
572
598
  }
573
599
 
574
- const instance = this.context.getInstance(dependent.instanceId)
600
+ const instance = this.getPhaseInstance(dependent.instanceId)
575
601
  dependentPromises.push(this.getInstancePromiseForOperation(instance, dependent))
576
602
  }
577
603
 
@@ -604,6 +630,7 @@ export class RuntimeOperation {
604
630
 
605
631
  await this.runnerBackend.destroy({
606
632
  projectId: this.project.id,
633
+ operationId: this.operation.id,
607
634
  stateId: state.id,
608
635
  libraryId: this.project.libraryId,
609
636
  instanceType: type,
@@ -613,6 +640,7 @@ export class RuntimeOperation {
613
640
  forceSignal,
614
641
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
615
642
  forceDeleteState: this.operation.options.forceDeleteState,
643
+ hasResourceHooks: state.hasResourceHooks ?? false,
616
644
  debug: this.operation.options.debug,
617
645
  })
618
646
 
@@ -639,6 +667,7 @@ export class RuntimeOperation {
639
667
 
640
668
  await this.runnerBackend.refresh({
641
669
  projectId: this.project.id,
670
+ operationId: this.operation.id,
642
671
  stateId: state.id,
643
672
  libraryId: this.project.libraryId,
644
673
  instanceType: type,
@@ -663,6 +692,7 @@ export class RuntimeOperation {
663
692
  ): Promise<void> {
664
693
  const stream = this.runnerBackend.watch({
665
694
  projectId: this.project.id,
695
+ operationId: this.operation.id,
666
696
  stateId: state.id,
667
697
  libraryId: this.project.libraryId,
668
698
  instanceType,
@@ -673,12 +703,18 @@ export class RuntimeOperation {
673
703
  let update: UnitStateUpdate | undefined
674
704
 
675
705
  for await (update of stream) {
706
+ let handlerError: Error | null = null
707
+
676
708
  try {
677
- await this.handleUnitStateUpdate(update, state)
709
+ handlerError = await this.handleUnitStateUpdate(update, state)
678
710
  } catch (error) {
679
711
  logger.error({ error }, "failed to handle unit state update")
680
712
  }
681
713
 
714
+ if (handlerError) {
715
+ throw handlerError
716
+ }
717
+
682
718
  if (update.type === "error") {
683
719
  // rethrow the error to stop the execution of dependent units
684
720
  throw new Error(
@@ -698,80 +734,121 @@ export class RuntimeOperation {
698
734
 
699
735
  private prepareUnitConfig(
700
736
  instance: InstanceModel,
737
+ stateId: string,
701
738
  secrets: Record<string, unknown>,
702
739
  invokedTriggers: TriggerInvocation[] = [],
703
740
  ): UnitConfig {
704
741
  const resolvedInputs = this.context.getResolvedInputs(instance.id)
742
+ const component = this.context.library.components[instance.type]!
743
+
744
+ const unfoldedInputs = mapValues(resolvedInputs ?? {}, (input, inputName) =>
745
+ input.flatMap(value => this.getUnitInputValues(inputName, value)),
746
+ )
747
+
748
+ for (const [inputName, inputSpec] of Object.entries(component.inputs)) {
749
+ if (inputSpec.multiple) {
750
+ continue
751
+ }
752
+
753
+ const values = unfoldedInputs[inputName] ?? []
754
+ if (values.length > 1) {
755
+ throw new Error(
756
+ `Input "${inputName}" of instance "${instance.id}" expects a single value, but ${values.length} values were resolved after unfolding.`,
757
+ )
758
+ }
759
+ }
705
760
 
706
761
  return {
707
762
  instanceId: instance.id,
763
+ stateId,
708
764
  args: instance.args ?? {},
709
- inputs: mapValues(resolvedInputs ?? {}, (input, inputName) =>
710
- input.map(value => this.getUnitInputRef(inputName, value)),
711
- ),
765
+ inputs: unfoldedInputs,
712
766
  invokedTriggers,
713
- secretNames: Object.keys(secrets),
714
- stateIdMap: this.context.getInstanceIdToStateIdMap(instance.id),
767
+ secretValues: secrets,
715
768
  importBasePath: this.libraryBackend.importPath,
716
769
  }
717
770
  }
718
771
 
719
- private getUnitInputRef(inputName: string, input: ResolvedInstanceInput): UnitInputReference {
720
- const instance = this.context.getInstance(input.input.instanceId)
721
- const component = this.context.library.components[instance.type]
772
+ private getUnitInputValues(inputName: string, input: ResolvedInstanceInput): UnitInputValue[] {
773
+ const dependencyInstance = this.context.getInstance(input.input.instanceId)
774
+ const captured = this.context.getCapturedOutputValues(
775
+ input.input.instanceId,
776
+ input.input.output,
777
+ )
722
778
 
723
- const outputSpec = component.outputs[input.input.output]
724
- if (!outputSpec) {
725
- throw new Error(
726
- `Output "${input.input.output}" is not defined on component "${instance.type}"`,
727
- )
728
- }
779
+ const dependencyComponent = this.context.library.components[dependencyInstance.type]
780
+ const fallbackType = dependencyComponent?.outputs[input.input.output]?.type ?? input.type
729
781
 
730
- const entity = this.context.library.entities[outputSpec.type]
731
- if (!entity) {
732
- throw new Error(`Entity type "${outputSpec.type}" is not defined in the library`)
733
- }
782
+ const getInstanceContext = (instanceId: string) => {
783
+ const resolvedOutput = this.context.inputResolver.outputs.get(`instance:${instanceId}`)
784
+ if (resolvedOutput && resolvedOutput.kind === "instance") {
785
+ return {
786
+ instance: resolvedOutput.instance,
787
+ component: resolvedOutput.component,
788
+ entities: resolvedOutput.entities,
789
+ }
790
+ }
791
+
792
+ try {
793
+ const instance = this.context.getInstance(instanceId as InstanceId)
794
+ const component = this.context.library.components[instance.type]
734
795
 
735
- // if output type matches input type or extends it, return simple reference, no transformation needed
736
- if (input.type === outputSpec.type || entity.extensions?.includes(input.type)) {
737
- return {
738
- instanceId: input.input.instanceId,
739
- output: input.input.output,
796
+ if (!component) {
797
+ return undefined
798
+ }
799
+
800
+ return {
801
+ instance,
802
+ component,
803
+ entities: this.context.library.entities,
804
+ }
805
+ } catch {
806
+ return undefined
740
807
  }
741
808
  }
742
809
 
743
- // otherwise, find matching inclusion to perform transformation
744
- const inclusion = entity.inclusions?.find(inc => inc.type === input.type)
745
- if (!inclusion) {
746
- throw new Error(
747
- `Cannot use output "${input.input.output}" of type "${outputSpec.type}" from instance "${input.input.instanceId}" for input "${inputName}" of type "${input.type}": no matching inclusion found in entity "${entity.type}"`,
748
- )
749
- }
810
+ const effectiveOutputType = resolveEffectiveOutputType({
811
+ input: input.input,
812
+ fallbackType,
813
+ getInstanceContext,
814
+ })
750
815
 
751
- return {
752
- instanceId: input.input.instanceId,
753
- output: input.input.output,
754
- inclusion,
755
- }
816
+ const effectiveRootOutputType = resolveEffectiveOutputType({
817
+ input: {
818
+ ...input.input,
819
+ path: undefined,
820
+ },
821
+ fallbackType,
822
+ getInstanceContext,
823
+ })
824
+
825
+ return resolveUnitInputValues({
826
+ library: this.context.library,
827
+ inputName,
828
+ resolvedInput: input,
829
+ dependencyInstanceType: dependencyInstance.type,
830
+ captured,
831
+ effectiveOutputType,
832
+ effectiveRootOutputType,
833
+ })
756
834
  }
757
835
 
758
836
  private async handleUnitStateUpdate(
759
837
  update: UnitStateUpdate,
760
838
  state: InstanceState,
761
- ): Promise<void> {
839
+ ): Promise<Error | null> {
762
840
  switch (update.type) {
763
841
  case "message":
764
842
  this.handleUnitMessage(update, state)
765
- return
843
+ return null
766
844
  case "progress":
767
845
  await this.handleUnitProgress(update)
768
- return
846
+ return null
769
847
  case "error":
770
848
  await this.handleUnitError(update, state)
771
- return
849
+ return null
772
850
  case "completion":
773
- await this.handleUnitCompletion(update, state)
774
- return
851
+ return await this.handleUnitCompletion(update, state)
775
852
  }
776
853
  }
777
854
 
@@ -811,7 +888,7 @@ export class RuntimeOperation {
811
888
  private async handleUnitCompletion(
812
889
  update: TypedUnitStateUpdate<"completion">,
813
890
  state: InstanceState,
814
- ): Promise<void> {
891
+ ): Promise<Error | null> {
815
892
  if (this.operation.type === "preview") {
816
893
  await this.workset.updateState(update.unitId, {
817
894
  operationState: {
@@ -819,24 +896,75 @@ export class RuntimeOperation {
819
896
  finishedAt: new Date(),
820
897
  },
821
898
  })
822
- return
899
+ return null
823
900
  }
824
901
 
825
- const instance = this.context.getInstance(update.unitId)
902
+ const instance = this.getPhaseInstance(update.unitId)
903
+
904
+ if (update.rawOutputs && update.operationType !== "destroy") {
905
+ this.context.updateCapturedOutputValuesFromUnitOutputs({
906
+ instanceId: instance.id,
907
+ instanceType: instance.type,
908
+ outputs: update.rawOutputs,
909
+ })
910
+ }
911
+
912
+ const parsed = update.rawOutputs
913
+ ? await this.unitOutputService.parseUnitOutputs({
914
+ libraryId: this.context.project.libraryId,
915
+ instanceType: instance.type,
916
+ outputs: update.rawOutputs,
917
+ })
918
+ : {
919
+ outputHash: null,
920
+ statusFields: null,
921
+ terminals: null,
922
+ pages: null,
923
+ triggers: null,
924
+ secrets: null,
925
+ workers: null,
926
+ exportedArtifactIds: null,
927
+ hasResourceHooks: false,
928
+ entitySnapshotError: null,
929
+ entitySnapshotPayload: null,
930
+ }
931
+
932
+ if (parsed.entitySnapshotError) {
933
+ await this.operationService.appendLog(
934
+ this.project.id,
935
+ this.operation.id,
936
+ state.id,
937
+ `Failed to parse unit outputs: ${parsed.entitySnapshotError}`,
938
+ )
939
+
940
+ await this.workset.updateState(update.unitId, {
941
+ instanceState: {
942
+ status: "failed",
943
+ },
944
+ operationState: {
945
+ status: "failed",
946
+ finishedAt: new Date(),
947
+ },
948
+ })
949
+
950
+ return new Error(
951
+ `Failed to parse unit outputs for unit "${instance.id}": ${parsed.entitySnapshotError}`,
952
+ )
953
+ }
826
954
 
827
955
  const data: InstanceStatePatch = {
828
956
  status: this.workset.getNextStableInstanceStatus(instance.id),
829
- statusFields: update.statusFields ?? null,
957
+ statusFields: parsed.statusFields,
830
958
  }
831
959
 
832
- const artifactIds = update.exportedArtifactIds
833
- ? Object.values(update.exportedArtifactIds).flat()
960
+ const artifactIds = parsed.exportedArtifactIds
961
+ ? Object.values(parsed.exportedArtifactIds).flat()
834
962
  : []
835
963
 
836
964
  if (update.operationType !== "destroy") {
837
965
  // давайте еще больше усложним и без того сложную штуку
838
966
  // set output hash before calculating input hash to capture up-to-date output hash for dependencies
839
- state.outputHash = update.outputHash ?? null
967
+ state.outputHash = parsed.outputHash
840
968
 
841
969
  // recalculate the input and output hashes for the instance
842
970
  const { selfHash, inputHash, dependencyOutputHash } =
@@ -845,9 +973,13 @@ export class RuntimeOperation {
845
973
  data.selfHash = selfHash
846
974
  data.inputHash = inputHash
847
975
  data.dependencyOutputHash = dependencyOutputHash
848
- data.outputHash = update.outputHash
976
+ data.outputHash = parsed.outputHash
849
977
 
850
- data.exportedArtifactIds = update.exportedArtifactIds
978
+ data.exportedArtifactIds = parsed.exportedArtifactIds
979
+
980
+ if (update.rawOutputs) {
981
+ data.hasResourceHooks = parsed.hasResourceHooks
982
+ }
851
983
 
852
984
  // also update the parent ID
853
985
  if (instance.parentId) {
@@ -865,6 +997,7 @@ export class RuntimeOperation {
865
997
  data.model = null
866
998
  data.resolvedInputs = null
867
999
  data.exportedArtifactIds = null
1000
+ data.hasResourceHooks = false
868
1001
  }
869
1002
 
870
1003
  // update the operation state
@@ -883,23 +1016,34 @@ export class RuntimeOperation {
883
1016
  // also do not write unit extra data for non-last phases of the instance
884
1017
  unitExtra: this.workset.isLastPhaseForInstance(instance.id)
885
1018
  ? {
886
- pages: update.pages ?? [],
887
- terminals: update.terminals ?? [],
888
- triggers: update.triggers ?? [],
889
- workers: update.workers ?? [],
890
- secrets: update.secrets ?? {},
1019
+ pages: parsed.pages ?? [],
1020
+ terminals: parsed.terminals ?? [],
1021
+ triggers: parsed.triggers ?? [],
1022
+ workers: parsed.workers ?? [],
1023
+ secrets: parsed.secrets ?? {},
891
1024
  artifactIds,
892
1025
  }
893
1026
  : undefined,
894
1027
  })
895
1028
 
1029
+ if (update.operationType !== "destroy" && parsed.entitySnapshotPayload) {
1030
+ await this.entitySnapshotService.persistUnitEntitySnapshots({
1031
+ projectId: this.project.id,
1032
+ operationId: this.operation.id,
1033
+ stateId: state.id,
1034
+ payload: parsed.entitySnapshotPayload,
1035
+ })
1036
+ }
1037
+
896
1038
  if (
897
1039
  update.operationType === "destroy" &&
898
1040
  this.workset.isLastPhaseForInstance(instance.id) &&
899
1041
  this.context.isGhostInstance(instance.id)
900
1042
  ) {
901
- this.instanceStateService.publishGhostInstanceDeletion(this.project.id, [instance.id])
1043
+ this.pendingGhostDeletionIds.add(instance.id)
902
1044
  }
1045
+
1046
+ return null
903
1047
  }
904
1048
 
905
1049
  private getInstancePromise(
@@ -1055,4 +1199,20 @@ export class RuntimeOperation {
1055
1199
 
1056
1200
  return Array.from(artifactIds)
1057
1201
  }
1202
+
1203
+ private getPhaseInstance(instanceId: InstanceId): InstanceModel {
1204
+ const modelInstance = this.context.tryGetInstance(instanceId)
1205
+ if (modelInstance) {
1206
+ return modelInstance
1207
+ }
1208
+
1209
+ if (this.workset.currentPhase === "destroy") {
1210
+ const destroyInstance = this.context.tryGetInstanceForDestroy(instanceId)
1211
+ if (destroyInstance) {
1212
+ return destroyInstance
1213
+ }
1214
+ }
1215
+
1216
+ throw new Error(`Instance with ID ${instanceId} not found in the operation context`)
1217
+ }
1058
1218
  }
@@ -18,6 +18,8 @@ describe("Operation - Update Failure", () => {
18
18
  instanceStateService,
19
19
  projectModelService,
20
20
  unitExtraService,
21
+ entitySnapshotService,
22
+ unitOutputService,
21
23
  createUnit,
22
24
  createDeployedUnitState,
23
25
  createOperation,
@@ -60,6 +62,8 @@ describe("Operation - Update Failure", () => {
60
62
  instanceStateService,
61
63
  projectModelService,
62
64
  unitExtraService,
65
+ entitySnapshotService,
66
+ unitOutputService,
63
67
  logger,
64
68
  )
65
69