@highstate/backend 0.9.16 → 0.9.19

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 (302) hide show
  1. package/dist/chunk-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/chunk-I7BWSAN6.js +49 -0
  4. package/dist/chunk-I7BWSAN6.js.map +1 -0
  5. package/dist/{chunk-RCB4AFGD.js → chunk-VB4YL327.js} +51 -71
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +5 -4
  10. package/dist/index.js +7676 -6634
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +8 -6
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +63 -58
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +3 -216
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +23 -11
  19. package/prisma/backend/_schema/layout.prisma +7 -0
  20. package/prisma/backend/_schema/library.prisma +17 -0
  21. package/prisma/backend/_schema/project.prisma +101 -0
  22. package/prisma/backend/_schema/pulumi.prisma +17 -0
  23. package/prisma/backend/postgresql/main.prisma +17 -0
  24. package/prisma/backend/sqlite/main.prisma +17 -0
  25. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  26. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  27. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  28. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  29. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  30. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  31. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  32. package/prisma/project/api-key.prisma +27 -0
  33. package/prisma/project/artifact.prisma +52 -0
  34. package/prisma/project/custom-status.prisma +46 -0
  35. package/prisma/project/evaluation.prisma +35 -0
  36. package/prisma/project/instance.prisma +160 -0
  37. package/prisma/project/layout.prisma +23 -0
  38. package/prisma/project/lock.prisma +18 -0
  39. package/prisma/project/main.prisma +17 -0
  40. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  41. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  42. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  43. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  45. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  46. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  47. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  48. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  49. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  50. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  51. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  52. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  53. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  54. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  55. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  56. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  59. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  60. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  61. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  62. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  64. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  65. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  66. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  67. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  68. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  69. package/prisma/project/migrations/migration_lock.toml +3 -0
  70. package/prisma/project/model.prisma +37 -0
  71. package/prisma/project/operation.prisma +148 -0
  72. package/prisma/project/page.prisma +41 -0
  73. package/prisma/project/secret.prisma +42 -0
  74. package/prisma/project/service-account.prisma +36 -0
  75. package/prisma/project/terminal.prisma +90 -0
  76. package/prisma/project/trigger.prisma +31 -0
  77. package/prisma/project/unlock-method.prisma +32 -0
  78. package/prisma/project/worker.prisma +138 -0
  79. package/src/artifact/abstractions.ts +13 -13
  80. package/src/artifact/encryption.ts +31 -15
  81. package/src/artifact/factory.ts +7 -10
  82. package/src/artifact/local.ts +33 -50
  83. package/src/business/api-key.ts +24 -36
  84. package/src/business/artifact.test.ts +978 -0
  85. package/src/business/artifact.ts +136 -215
  86. package/src/business/evaluation.ts +328 -0
  87. package/src/business/index.ts +5 -1
  88. package/src/business/instance-lock.test.ts +1060 -0
  89. package/src/business/instance-lock.ts +387 -77
  90. package/src/business/instance-state.test.ts +735 -0
  91. package/src/business/instance-state.ts +604 -217
  92. package/src/business/operation.test.ts +439 -0
  93. package/src/business/operation.ts +174 -208
  94. package/src/business/project-model.ts +258 -0
  95. package/src/business/project-unlock.ts +172 -112
  96. package/src/business/project.ts +407 -0
  97. package/src/business/secret.test.ts +513 -0
  98. package/src/business/secret.ts +194 -131
  99. package/src/business/settings.test.ts +695 -0
  100. package/src/business/settings.ts +855 -0
  101. package/src/business/terminal-session.ts +90 -0
  102. package/src/business/unit-extra.test.ts +539 -0
  103. package/src/business/unit-extra.ts +160 -0
  104. package/src/business/worker.test.ts +391 -0
  105. package/src/business/worker.ts +250 -114
  106. package/src/common/codebase.ts +65 -0
  107. package/src/common/index.ts +3 -2
  108. package/src/common/logger.ts +5 -0
  109. package/src/common/utils.ts +4 -3
  110. package/src/config.ts +15 -12
  111. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  112. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  113. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  114. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  115. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  116. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  117. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  119. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  120. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  121. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  122. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  123. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  124. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  125. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  126. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  127. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  128. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  129. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  130. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  132. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  133. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  134. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  135. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  136. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  137. package/src/database/_generated/project/client.ts +204 -0
  138. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  139. package/src/database/_generated/project/enums.ts +104 -0
  140. package/src/database/_generated/project/internal/class.ts +479 -0
  141. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  142. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  143. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  144. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  145. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  146. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  147. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  148. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  149. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  150. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  151. package/src/database/_generated/project/models/Operation.ts +1647 -0
  152. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  153. package/src/database/_generated/project/models/Page.ts +1838 -0
  154. package/src/database/_generated/project/models/Secret.ts +1692 -0
  155. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  156. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  157. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  158. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  159. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  160. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  161. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  162. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  163. package/src/database/_generated/project/models/Worker.ts +1459 -0
  164. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  165. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  166. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  167. package/src/database/_generated/project/models.ts +35 -0
  168. package/src/database/_generated/project/pjtg.ts +182 -0
  169. package/src/database/abstractions.ts +19 -0
  170. package/src/database/factory.ts +37 -0
  171. package/src/database/index.ts +6 -0
  172. package/src/database/local/backend.ts +134 -0
  173. package/src/database/local/index.ts +3 -0
  174. package/src/database/local/meta.ts +46 -0
  175. package/src/database/local/prisma.config.ts +25 -0
  176. package/src/database/local/project.ts +39 -0
  177. package/src/database/manager.ts +181 -0
  178. package/src/database/migrate.ts +35 -0
  179. package/src/database/prisma.ts +56 -0
  180. package/src/database/well-known.ts +38 -0
  181. package/src/index.ts +4 -4
  182. package/src/library/abstractions.ts +21 -14
  183. package/src/library/factory.ts +1 -1
  184. package/src/library/local.ts +86 -38
  185. package/src/library/package-resolution-worker.ts +1 -1
  186. package/src/library/worker/evaluator.ts +61 -48
  187. package/src/library/worker/loader.lite.ts +14 -1
  188. package/src/library/worker/main.ts +9 -16
  189. package/src/library/worker/protocol.ts +0 -12
  190. package/src/lock/manager.ts +12 -7
  191. package/src/orchestrator/manager.ts +198 -131
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +235 -583
  203. package/src/orchestrator/operation.ts +446 -904
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +49 -25
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +38 -26
  219. package/src/runner/artifact-env.ts +17 -6
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +79 -72
  223. package/src/runner/pulumi.ts +26 -63
  224. package/src/services.ts +214 -103
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +12 -4
  227. package/src/shared/models/backend/project.ts +43 -23
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -109
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -56
  236. package/src/shared/models/project/artifact.ts +15 -105
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +9 -9
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +32 -0
  241. package/src/shared/models/project/operation.ts +222 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -103
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -390
  246. package/src/shared/models/project/terminal.ts +75 -89
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +21 -20
  249. package/src/shared/models/project/worker.ts +89 -88
  250. package/src/shared/resolvers/graph-resolver.ts +62 -26
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +48 -6
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +29 -9
  257. package/src/shared/utils/index.ts +1 -1
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +31 -0
  266. package/src/unlock/index.ts +2 -0
  267. package/src/unlock/memory.ts +27 -0
  268. package/src/worker/abstractions.ts +7 -4
  269. package/src/worker/docker.ts +14 -19
  270. package/src/worker/manager.ts +376 -79
  271. package/dist/chunk-RCB4AFGD.js.map +0 -1
  272. package/dist/chunk-WHALQHEZ.js +0 -2017
  273. package/dist/chunk-WHALQHEZ.js.map +0 -1
  274. package/src/business/backend-unlock.ts +0 -10
  275. package/src/common/performance.ts +0 -44
  276. package/src/hotstate/abstractions.ts +0 -48
  277. package/src/hotstate/factory.ts +0 -17
  278. package/src/hotstate/index.ts +0 -3
  279. package/src/hotstate/manager.ts +0 -192
  280. package/src/hotstate/memory.ts +0 -100
  281. package/src/hotstate/validation.ts +0 -101
  282. package/src/project/abstractions.ts +0 -102
  283. package/src/project/factory.ts +0 -11
  284. package/src/project/index.ts +0 -3
  285. package/src/project/local.ts +0 -469
  286. package/src/project/manager.ts +0 -574
  287. package/src/pubsub/local.ts +0 -36
  288. package/src/pubsub/validation.ts +0 -33
  289. package/src/shared/models/project/component.ts +0 -45
  290. package/src/shared/models/project/instance.ts +0 -74
  291. package/src/state/abstractions.ts +0 -450
  292. package/src/state/encryption.ts +0 -59
  293. package/src/state/factory.ts +0 -20
  294. package/src/state/index.ts +0 -6
  295. package/src/state/local/backend.ts +0 -299
  296. package/src/state/local/collection.ts +0 -342
  297. package/src/state/local/index.ts +0 -2
  298. package/src/state/manager.ts +0 -819
  299. package/src/state/repository/index.ts +0 -2
  300. package/src/state/repository/repository.index.ts +0 -193
  301. package/src/state/repository/repository.ts +0 -458
  302. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -1,499 +1,236 @@
1
- import type { LibraryBackend } from "../library"
2
- import type { ProjectBackend } from "../project"
3
- import type { StateManager } from "../state"
4
- import type { Logger } from "pino"
5
- import type { InstanceLockService, InstanceStateService } from "../business"
1
+ import type { InstanceId } from "@highstate/contract"
2
+ import type { InstanceStateService, UpdateOperationStateOptions } from "../business"
3
+ import type { InstanceOperationStatus } from "../database"
4
+ import type { InstanceStatus, OperationPhase, OperationPhaseType, ProjectOutput } from "../shared"
5
+ import type { OperationContext } from "./operation-context"
6
6
  import { EventEmitter, on } from "node:events"
7
- import {
8
- isUnitModel,
9
- parseInstanceId,
10
- type ComponentModel,
11
- type InstanceModel,
12
- } from "@highstate/contract"
13
- import { unique } from "remeda"
14
- import { BetterLock } from "better-lock"
15
- import {
16
- InputHashResolver,
17
- InputResolver,
18
- type InputHashNode,
19
- type InputHashOutput,
20
- type InputResolverNode,
21
- type InstanceOperataionStatusEnum,
22
- type InstanceState,
23
- type InstanceStatePatch,
24
- type LibraryModel,
25
- type Operation,
26
- type ResolvedInstanceInput,
27
- } from "../shared"
28
-
29
- export type OperationPhase = "update" | "destroy" | "refresh"
7
+ import { mapValues } from "remeda"
30
8
 
31
- export class OperationWorkset {
32
- private readonly lockedInstanceIds = new Set<string>()
33
- public readonly instanceIdsToLockIds = new Set<string>()
34
-
35
- private readonly instanceIdsToUpdate = new Set<string>()
36
- private readonly instanceIdsToDestroy = new Set<string>()
37
-
38
- private readonly instanceMap = new Map<string, InstanceModel>()
39
- private readonly instanceChildrenMap = new Map<string, InstanceModel[]>()
40
-
41
- private readonly initialStateMap = new Map<string, InstanceState>()
42
- private readonly stateMap = new Map<string, InstanceState>()
43
- private readonly dependentStateIdMap = new Map<string, Set<string>>()
44
- private readonly stateChildIdMap = new Map<string, string[]>()
45
-
46
- private readonly unitSourceHashMap = new Map<string, number>()
47
-
48
- private inputResolver!: InputResolver
49
- private readonly inputResolverNodes = new Map<string, InputResolverNode>()
50
-
51
- private inputHashResolver!: InputHashResolver
52
- private readonly inputHashNodes = new Map<string, InputHashNode>()
53
- private readonly inputHashResolverLock = new BetterLock()
9
+ type AbortControllerPair = {
10
+ abortController: AbortController
11
+ forceAbortController: AbortController
12
+ }
54
13
 
55
- public readonly resolvedInstanceInputs = new Map<
56
- string,
57
- Record<string, ResolvedInstanceInput[]>
58
- >()
14
+ export class OperationWorkset {
15
+ readonly abortController = new AbortController()
16
+ private readonly forceAbortController = new AbortController()
59
17
 
60
- public currentPhase!: OperationPhase
18
+ readonly instanceAbortControllers = new Map<string, AbortControllerPair>()
61
19
 
20
+ private readonly lockedStateIds = new Set<string>()
62
21
  private readonly lockEventEmitter = new EventEmitter()
63
-
64
- private constructor(
65
- public readonly projectId: string,
66
- public readonly operation: Operation,
67
- public readonly library: LibraryModel,
68
- public readonly libraryId: string,
69
- private readonly instanceLockService: InstanceLockService,
22
+ private currentPhaseIndex = -1
23
+
24
+ currentPhase!: OperationPhaseType
25
+ allAffectedInstanceIds!: ReadonlySet<InstanceId>
26
+ allAffectedStateIds!: ReadonlySet<string>
27
+ phaseAffectedInstanceIds!: ReadonlySet<InstanceId>
28
+
29
+ constructor(
30
+ readonly project: ProjectOutput,
31
+ readonly operationId: string,
32
+ readonly phases: OperationPhase[],
33
+ private readonly context: OperationContext,
70
34
  private readonly instanceStateService: InstanceStateService,
71
- private readonly logger: Logger,
72
- ) {}
35
+ ) {
36
+ const affectedInstanceIds = new Set<InstanceId>()
37
+ const affectedStateIds = new Set<string>()
73
38
 
74
- public async ensureInstanceLocked(instanceId: string, signal: AbortSignal): Promise<void> {
75
- if (this.lockedInstanceIds.has(instanceId)) {
76
- return
77
- }
39
+ for (const phase of phases) {
40
+ for (const instance of phase.instances) {
41
+ affectedInstanceIds.add(instance.id)
78
42
 
79
- for await (const _ of on(this.lockEventEmitter, instanceId, { signal })) {
80
- return
43
+ const state = this.context.getState(instance.id)
44
+ affectedStateIds.add(state.id)
45
+ }
81
46
  }
82
- }
83
47
 
84
- public getInstance(instanceId: string): InstanceModel {
85
- const instance = this.instanceMap.get(instanceId)
86
- if (!instance) {
87
- throw new Error(`Instance with ID ${instanceId} not found in the operation workset`)
88
- }
48
+ this.allAffectedInstanceIds = affectedInstanceIds
49
+ this.allAffectedStateIds = affectedStateIds
89
50
 
90
- return instance
51
+ // we will basically create one listener per affected instance
52
+ this.lockEventEmitter.setMaxListeners(this.allAffectedInstanceIds.size)
91
53
  }
92
54
 
93
- public getAffectedInstanceIds(): string[] {
94
- if (this.currentPhase === "destroy") {
95
- return Array.from(this.instanceIdsToDestroy)
96
- }
97
-
98
- return Array.from(this.instanceIdsToUpdate)
55
+ hasRemainingPhases(): boolean {
56
+ return this.currentPhaseIndex < this.phases.length - 1
99
57
  }
100
58
 
101
- public getAllAffectedInstanceIds(): string[] {
102
- return Array.from(this.instanceIdsToUpdate.union(this.instanceIdsToDestroy))
59
+ getLockedStateIds(): Iterable<string> {
60
+ return this.lockedStateIds
103
61
  }
104
62
 
105
- public getInstanceOrUndefined(instanceId: string): InstanceModel | undefined {
106
- return this.instanceMap.get(instanceId)
107
- }
63
+ isLastPhaseForInstance(instanceId: InstanceId): boolean {
64
+ if (!this.hasRemainingPhases()) {
65
+ return true
66
+ }
108
67
 
109
- public isAffected(instanceId: string): boolean {
110
- if (this.currentPhase === "destroy") {
111
- return this.instanceIdsToDestroy.has(instanceId)
68
+ // TODO: create instance id sets for each phase on initialization to speed this up
69
+ for (let i = this.currentPhaseIndex + 1; i < this.phases.length; i++) {
70
+ if (this.phases[i].instances.find(i => i.id === instanceId)) {
71
+ return false
72
+ }
112
73
  }
113
74
 
114
- return this.instanceIdsToUpdate.has(instanceId)
75
+ return true
115
76
  }
116
77
 
117
- public async patchState(patch: InstanceStatePatch): Promise<InstanceState> {
118
- const state = await this.instanceStateService.patchOperationInstanceState(
119
- this.projectId,
120
- this.operation,
121
- patch,
122
- )
123
- this.stateMap.set(state.id, state)
124
-
125
- if (state.parentId) {
126
- await this.recalculateCompositeInstanceState(state.parentId)
78
+ nextPhase(): void {
79
+ if (!this.hasRemainingPhases()) {
80
+ throw new Error("No remaining phases")
127
81
  }
128
82
 
129
- return state
130
- }
131
-
132
- public setState(state: InstanceState): void {
133
- this.stateMap.set(state.id, state)
134
- this.initialStateMap.set(state.id, state)
83
+ this.currentPhaseIndex++
84
+ this.currentPhase = this.phases[this.currentPhaseIndex].type
135
85
 
136
- if (state.parentId) {
137
- let children = this.stateChildIdMap.get(state.parentId)
138
- if (!children) {
139
- children = []
140
- this.stateChildIdMap.set(state.parentId, children)
141
- }
86
+ this.phaseAffectedInstanceIds = new Set(
87
+ this.phases[this.currentPhaseIndex].instances.map(i => i.id),
88
+ )
89
+ }
142
90
 
143
- children.push(state.id)
91
+ async waitForInstanceLock(stateId: string, signal: AbortSignal): Promise<void> {
92
+ if (this.lockedStateIds.has(stateId)) {
93
+ return
144
94
  }
145
95
 
146
- for (const dependencyId of state.operationStatus?.dependencyIds ?? []) {
147
- this.addDependentState(state.id, dependencyId)
96
+ for await (const _ of on(this.lockEventEmitter, stateId, { signal })) {
97
+ return
148
98
  }
149
99
  }
150
100
 
151
- private addDependentState(instanceId: string, dependencyId: string): void {
152
- let dependentStates = this.dependentStateIdMap.get(dependencyId)
153
-
154
- if (!dependentStates) {
155
- dependentStates = new Set<string>()
156
- this.dependentStateIdMap.set(dependencyId, dependentStates)
157
- }
101
+ markInstanceLocked(stateId: string): void {
102
+ this.lockedStateIds.add(stateId)
103
+ this.lockEventEmitter.emit(stateId)
104
+ }
158
105
 
159
- dependentStates.add(instanceId)
106
+ markInstanceUnlocked(stateId: string): void {
107
+ this.lockedStateIds.delete(stateId)
160
108
  }
161
109
 
162
- public async restoreInitialState(instanceId: string): Promise<void> {
163
- const initialState = this.initialStateMap.get(instanceId) ?? { id: instanceId }
110
+ async setupOperationStates(): Promise<void> {
111
+ const patches = await this.instanceStateService.createOperationStates(
112
+ this.project.id,
113
+ Array.from(this.allAffectedInstanceIds).map(instanceId => {
114
+ const instance = this.context.getInstance(instanceId)
115
+ const state = this.context.getState(instanceId)
116
+
117
+ const resolvedInputs = mapValues(
118
+ //
119
+ this.context.getResolvedInputs(instance.id) ?? {},
120
+ inputs => inputs.map(input => input.input),
121
+ )
164
122
 
165
- this.logger.debug(
166
- { initialState },
167
- `resetting state for instance "%s" to initial state`,
168
- instanceId,
123
+ return [
124
+ {
125
+ stateId: state.id,
126
+ operationId: this.operationId,
127
+ status: "pending",
128
+ model: instance,
129
+ resolvedInputs,
130
+ },
131
+ {
132
+ status: state.status === "undeployed" ? "attempted" : state.status,
133
+ model: instance,
134
+ resolvedInputs,
135
+ },
136
+ ]
137
+ }),
169
138
  )
170
139
 
171
- await this.instanceStateService.updateInstanceStates(this.projectId, [initialState], true)
172
-
173
- if (initialState.parentId) {
174
- await this.recalculateCompositeInstanceState(initialState.parentId)
140
+ for (const patch of patches) {
141
+ // biome-ignore lint/style/noNonNullAssertion: we know it's there (should be (please))
142
+ const state = this.context.getState(patch.instanceId!)
143
+ Object.assign(state, patch)
175
144
  }
176
145
  }
177
146
 
178
- public async restoreAffectedInitialStates(): Promise<void> {
179
- const instanceStates: InstanceState[] = []
180
-
181
- for (const instanceId of this.getAffectedInstanceIds()) {
182
- const state = this.initialStateMap.get(instanceId)
183
- if (state) {
184
- instanceStates.push(state)
185
- }
186
- }
147
+ async updateState(instanceId: InstanceId, options: UpdateOperationStateOptions): Promise<void> {
148
+ const state = this.context.getState(instanceId)
187
149
 
188
- await this.instanceStateService.updateInstanceStates(this.projectId, instanceStates)
189
- }
150
+ const patch = await this.instanceStateService.updateOperationState(
151
+ this.project.id,
152
+ state.id,
153
+ this.operationId,
154
+ options,
155
+ )
190
156
 
191
- public getAffectedCompositeChildren(instanceId: string): InstanceModel[] {
192
- const children = this.instanceChildrenMap.get(instanceId)
193
- if (!children) {
194
- return []
157
+ if (state.parentInstanceId) {
158
+ // TODO: update all updates in single transaction
159
+ await this.recalculateCompositeInstanceState(state.parentInstanceId)
195
160
  }
196
161
 
197
- if (this.currentPhase === "destroy") {
198
- return children.filter(child => this.instanceIdsToDestroy.has(child.id))
199
- }
200
-
201
- return children.filter(child => this.instanceIdsToUpdate.has(child.id))
162
+ Object.assign(state, patch)
202
163
  }
203
164
 
204
- public getState(instanceId: string): InstanceState | undefined {
205
- return this.stateMap.get(instanceId)
206
- }
207
-
208
- public getParentId(instanceId: string): string | undefined {
165
+ getAffectedCompositeChildren(instanceId: InstanceId): InstanceId[] {
209
166
  if (this.currentPhase === "destroy") {
210
- const state = this.stateMap.get(instanceId)
211
- if (!state) {
212
- return undefined
213
- }
214
-
215
- return state.parentId
216
- }
217
-
218
- const instance = this.getInstance(instanceId)
219
-
220
- return instance.parentId
221
- }
222
-
223
- public getDependentStates(instanceId: string): InstanceState[] {
224
- const dependentStateIds = this.dependentStateIdMap.get(instanceId)
225
- if (!dependentStateIds) {
226
- return []
167
+ // when destroying, only consider children fixed in the state
168
+ return this.context
169
+ .getStateChildIds(instanceId)
170
+ .filter(child => this.phaseAffectedInstanceIds.has(child))
227
171
  }
228
172
 
229
- return Array.from(dependentStateIds)
230
- .map(id => this.stateMap.get(id))
231
- .filter((state): state is InstanceState => !!state)
173
+ // for other phases, consider all children defined in the model
174
+ return this.context
175
+ .getInstanceChildren(instanceId)
176
+ .map(child => child.id)
177
+ .filter(childId => this.phaseAffectedInstanceIds.has(childId))
232
178
  }
233
179
 
234
- private async recalculateCompositeInstanceState(instanceId: string): Promise<void> {
235
- const state = this.stateMap.get(instanceId)
236
- if (!state) {
237
- this.logger.warn(
238
- `cannot recalculate composite instance state for "${instanceId}" because it is not in the state map`,
239
- )
240
- return
241
- }
180
+ private async recalculateCompositeInstanceState(instanceId: InstanceId): Promise<void> {
181
+ const state = this.context.getState(instanceId)
242
182
 
243
183
  let currentResourceCount = 0
244
184
  let totalResourceCount = 0
185
+ let knownTotatalResourceCount = 0
245
186
 
246
- const children = this.stateChildIdMap.get(instanceId) ?? []
187
+ const children = this.context.getStateChildIds(instanceId)
247
188
  for (const childId of children) {
248
- const child = this.stateMap.get(childId)
189
+ const child = this.context.getState(childId)
249
190
 
250
- if (child?.operationStatus?.currentResourceCount) {
251
- currentResourceCount += child.operationStatus.currentResourceCount
191
+ if (child?.lastOperationState?.currentResourceCount) {
192
+ currentResourceCount += child.lastOperationState.currentResourceCount
252
193
  }
253
194
 
254
- if (child?.operationStatus?.totalResourceCount) {
255
- totalResourceCount += child.operationStatus?.totalResourceCount
195
+ if (child?.lastOperationState?.totalResourceCount) {
196
+ totalResourceCount += child.lastOperationState.totalResourceCount
197
+ knownTotatalResourceCount += 1
256
198
  }
257
199
  }
258
200
 
259
- const patch: InstanceStatePatch = {
260
- id: instanceId,
261
- operationStatus: {
262
- status: this.getTransientStatusByOperationPhase(),
263
- currentResourceCount,
264
- totalResourceCount,
265
- },
266
- }
201
+ // extrapolate total resource count for other resources without total resource count
202
+ const averageTotalResourceCount =
203
+ knownTotatalResourceCount > 0 ? Math.round(totalResourceCount / knownTotatalResourceCount) : 0
267
204
 
268
- await this.patchState(patch)
269
- }
205
+ const notKnownTotalResourceCount = children.length - knownTotatalResourceCount
206
+ totalResourceCount += notKnownTotalResourceCount * averageTotalResourceCount
270
207
 
271
- private addInstance(instance: InstanceModel): void {
272
- if (this.instanceMap.has(instance.id)) {
273
- throw new Error(`Found multiple instances with the same ID: ${instance.id}`)
274
- }
208
+ const finalTotalResourceCount =
209
+ this.currentPhase === "destroy" && state.lastOperationState?.totalResourceCount
210
+ ? // do not override totalResourceCount with lower values when destroying instances
211
+ Math.min(totalResourceCount, state.lastOperationState.totalResourceCount)
212
+ : totalResourceCount
275
213
 
276
- if (!(instance.type in this.library.components)) {
277
- this.logger.warn(
278
- `ignoring instance "${instance.id}" because its type "${instance.type}" is not in the library`,
279
- )
280
- return
281
- }
282
-
283
- this.instanceMap.set(instance.id, instance)
284
-
285
- if (instance.parentId) {
286
- let children = this.instanceChildrenMap.get(instance.parentId)
287
- if (!children) {
288
- children = []
289
- this.instanceChildrenMap.set(instance.parentId, children)
290
- }
291
-
292
- children.push(instance)
293
- }
294
- }
295
-
296
- private async calculateInstanceIdsToUpdate(): Promise<void> {
297
- const traverse = async (instanceId: string) => {
298
- if (this.instanceIdsToUpdate.has(instanceId)) {
299
- return
300
- }
301
-
302
- const instance = this.instanceMap.get(instanceId)
303
- if (!instance) {
304
- return
305
- }
306
-
307
- const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {}
308
-
309
- for (const inputs of Object.values(instanceInputs)) {
310
- for (const input of inputs) {
311
- await traverse(input.input.instanceId)
312
- }
313
- }
314
-
315
- const state = this.stateMap.get(instance.id)
316
- const { inputHash: expectedInputHash } = this.inputHashResolver.requireOutput(instance.id)
317
-
318
- if (this.operation.options.forceUpdateDependencies) {
319
- this.instanceIdsToUpdate.add(instanceId)
320
- return
321
- }
322
-
323
- if (
324
- !state?.operationStatus ||
325
- state.operationStatus.status === "error" ||
326
- state.operationStatus.inputHash !== expectedInputHash
327
- ) {
328
- this.instanceIdsToUpdate.add(instanceId)
329
- }
330
- }
331
-
332
- // 1. extend affected instance IDs with their not-created or not-up-to-date dependencies (only for "update" operations)
333
- for (const instanceId of this.operation.requestedInstanceIds) {
334
- if (this.operation.type === "update") {
335
- await traverse(instanceId)
336
- }
337
-
338
- this.instanceIdsToUpdate.add(instanceId)
339
- }
340
-
341
- // 2. extend affected instance IDs with the children of the affected composite instances
342
- const compositeInstanceQueue = Array.from(this.instanceIdsToUpdate)
343
- while (compositeInstanceQueue.length > 0) {
344
- const childId = compositeInstanceQueue.pop()!
345
- const children = this.instanceChildrenMap.get(childId) ?? []
346
-
347
- for (const child of children) {
348
- compositeInstanceQueue.push(child.id)
349
-
350
- if (this.operation.options.forceUpdateChildren) {
351
- this.instanceIdsToUpdate.add(child.id)
352
- continue
353
- }
354
-
355
- const state = this.stateMap.get(child.id)
356
- const { inputHash: expectedInputHash } = this.inputHashResolver.requireOutput(child.id)
357
-
358
- if (
359
- !state?.operationStatus ||
360
- state.operationStatus.status === "error" ||
361
- state.operationStatus.inputHash !== expectedInputHash
362
- ) {
363
- this.instanceIdsToUpdate.add(child.id)
364
- }
365
- }
366
- }
367
-
368
- // 3. detect composite instance children and include their parents (recursively)
369
- for (const instanceId of this.instanceIdsToUpdate) {
370
- let instance = this.instanceMap.get(instanceId)
371
- while (instance?.parentId) {
372
- this.instanceIdsToUpdate.add(instance.parentId)
373
- instance = this.instanceMap.get(instance.parentId)
374
- }
375
- }
376
-
377
- // 4. if "allowPartialCompositeInstanceUpdates" is not set, include all children of the composite instances
378
- if (!this.operation.options.allowPartialCompositeInstanceUpdates) {
379
- for (const instanceId of this.instanceIdsToUpdate) {
380
- const children = this.instanceChildrenMap.get(instanceId) ?? []
381
- for (const child of children) {
382
- this.instanceIdsToUpdate.add(child.id)
383
- }
384
- }
385
- }
386
-
387
- this.operation.instanceIdsToUpdate = Array.from(this.instanceIdsToUpdate)
388
- }
389
-
390
- private calculateInstanceIdsToDestroy() {
391
- const traverse = (instanceKey: string) => {
392
- if (this.instanceIdsToDestroy.has(instanceKey)) {
393
- return
394
- }
395
-
396
- const state = this.stateMap.get(instanceKey)
397
- if (!state) {
398
- return
399
- }
400
-
401
- const dependentIds = this.dependentStateIdMap.get(instanceKey) ?? []
402
-
403
- for (const dependentId of dependentIds) {
404
- traverse(dependentId)
405
- this.instanceIdsToDestroy.add(dependentId)
406
- }
407
- }
408
-
409
- if (this.operation.type === "destroy" || this.operation.type === "recreate") {
410
- // 1.a. extend affected instance IDs with their created dependents (if not forbidden by the operation options)
411
- for (const instanceId of this.operation.requestedInstanceIds) {
412
- const instance = this.instanceMap.get(instanceId)
413
- if (!instance) {
414
- throw new Error(`Instance not found: ${instanceId}`)
415
- }
416
-
417
- if (this.operation.options.destroyDependentInstances) {
418
- traverse(instance.id)
419
- }
420
-
421
- this.instanceIdsToDestroy.add(instanceId)
422
- }
423
- } else if (this.operation.type === "update") {
424
- // 1.b. find all children instances of the affected to-update instances which are in the state map, but not in the instance map
425
- // in other words, this code cleans up child instances which are not produced by composite instances anymore
426
-
427
- for (const instanceId of this.operation.instanceIdsToUpdate) {
428
- const [type] = parseInstanceId(instanceId)
429
- const component = this.library.components[type]
430
-
431
- if (!component || isUnitModel(component)) {
432
- // ignore non-composite instances
433
- continue
434
- }
435
-
436
- const childrenQueue = [...(this.stateChildIdMap.get(instanceId) ?? [])]
437
-
438
- while (childrenQueue.length > 0) {
439
- const childId = childrenQueue.pop()!
440
- if (!this.instanceMap.has(childId)) {
441
- this.instanceIdsToDestroy.add(childId)
442
- }
443
-
444
- childrenQueue.push(...(this.stateChildIdMap.get(childId) ?? []))
445
- }
446
- }
447
- } else {
448
- return
449
- }
450
-
451
- // 2. extend affected instance IDs with the children of the affected composite instances
452
- const compositeInstanceQueue = Array.from(this.instanceIdsToDestroy)
453
- while (compositeInstanceQueue.length > 0) {
454
- const childId = compositeInstanceQueue.pop()!
455
- const children = this.stateChildIdMap.get(childId) ?? []
456
-
457
- for (const child of children) {
458
- compositeInstanceQueue.push(child)
459
- this.instanceIdsToDestroy.add(child)
460
- }
461
- }
462
-
463
- // 3. detect composite instance children and include their parents (recursively)
464
- for (const instanceId of this.instanceIdsToDestroy) {
465
- let state = this.stateMap.get(instanceId)
466
- while (state?.parentId) {
467
- this.instanceIdsToDestroy.add(state.parentId)
468
- state = this.stateMap.get(state.parentId)
469
- }
470
- }
471
-
472
- // 4. if "allowPartialCompositeInstanceUpdates" is not set, include all children of the composite instances
473
- if (!this.operation.options.allowPartialCompositeInstanceUpdates) {
474
- for (const instanceId of this.instanceIdsToDestroy) {
475
- const children = this.stateChildIdMap.get(instanceId) ?? []
476
- for (const childId of children) {
477
- this.instanceIdsToDestroy.add(childId)
478
- }
479
- }
480
- }
481
-
482
- this.operation.instanceIdsToDestroy = Array.from(this.instanceIdsToDestroy)
214
+ await this.updateState(instanceId, {
215
+ operationState: {
216
+ status: this.getTransientStatusByOperationPhase(),
217
+ currentResourceCount,
218
+ totalResourceCount: finalTotalResourceCount,
219
+ },
220
+ })
483
221
  }
484
222
 
485
- private getSourceHashIfApplicable(
486
- instance: InstanceModel,
487
- component: ComponentModel,
488
- ): number | undefined {
489
- if (isUnitModel(component)) {
490
- return this.unitSourceHashMap.get(instance.type)
223
+ getPhaseParentId(instanceId: InstanceId): InstanceId | null {
224
+ if (this.currentPhase === "destroy") {
225
+ // when destroying, only consider parent fixed in the state
226
+ return this.context.getState(instanceId).parentInstanceId ?? null
491
227
  }
492
228
 
493
- return undefined
229
+ // for other phases, consider parent defined in the model
230
+ return this.context.getInstance(instanceId).parentId ?? null
494
231
  }
495
232
 
496
- private getTransientStatusByOperationPhase(): InstanceOperataionStatusEnum {
233
+ getTransientStatusByOperationPhase(): InstanceOperationStatus {
497
234
  switch (this.currentPhase) {
498
235
  case "update":
499
236
  return "updating"
@@ -504,193 +241,108 @@ export class OperationWorkset {
504
241
  }
505
242
  }
506
243
 
507
- public async getUpToDateInputHashOutput(instance: InstanceModel): Promise<InputHashOutput> {
508
- return await this.inputHashResolverLock.acquire(async () => {
509
- const component = this.library.components[instance.type]
510
-
511
- this.inputHashNodes.set(instance.id, {
512
- instance,
513
- component,
514
- resolvedInputs: this.resolvedInstanceInputs.get(instance.id)!,
515
- state: this.stateMap.get(instance.id),
516
- sourceHash: this.getSourceHashIfApplicable(instance, component),
517
- })
518
-
519
- this.inputHashResolver.invalidate(instance.id)
520
- await this.inputHashResolver.process()
521
-
522
- return this.inputHashResolver.requireOutput(instance.id)
523
- })
524
- }
525
-
526
- /**
527
- * Tries to acquire not-locked instances in the workset.
528
- *
529
- * Should not be called if the whole workset is already locked.
530
- *
531
- * If `instancesToLock` is provided, it will try to lock only those instances.
532
- */
533
- public async tryLock(instancesToLock?: string[]): Promise<void> {
534
- const [, lockedInstanceIds] = await this.instanceLockService.tryLockInstances(
535
- this.projectId,
536
- instancesToLock ?? Array.from(this.instanceIdsToLockIds),
537
- {
538
- title: "Operation Lock",
539
- description: `The instance is locked for the ${this.operation.type} operation with ID "${this.operation.id}".`,
540
- icon: "mdi:cog-sync",
541
- },
542
- { type: "operation", operationId: this.operation.id },
543
- true, // allow partial locking
544
- )
545
-
546
- // add locked instance IDs to the workset and remove them from the instanceIdsToLockIds
547
- for (const instanceId of lockedInstanceIds) {
548
- this.lockedInstanceIds.add(instanceId)
549
- this.instanceIdsToLockIds.delete(instanceId)
550
- this.lockEventEmitter.emit(instanceId, null)
244
+ getStableStatusByOperationPhase(): InstanceOperationStatus {
245
+ switch (this.currentPhase) {
246
+ case "update":
247
+ return "updated"
248
+ case "destroy":
249
+ return "destroyed"
250
+ case "refresh":
251
+ return "refreshed"
551
252
  }
552
253
  }
553
254
 
554
- public static async load(
555
- projectId: string,
556
- operation: Operation,
557
- projectBackend: ProjectBackend,
558
- libraryBackend: LibraryBackend,
559
- stateManager: StateManager,
560
- instanceLockService: InstanceLockService,
561
- instanceStateService: InstanceStateService,
562
- logger: Logger,
563
- signal: AbortSignal,
564
- ): Promise<OperationWorkset> {
565
- // First get project info to obtain libraryId
566
- const projectInfo = await projectBackend.getProjectInfo(projectId, signal)
567
-
568
- // Use StateManager for instance states, StateBackend directly for composite instances
569
- // TODO: use hotstate for composite instances as well
570
- const compositeInstanceRepo = stateManager.getCompositeInstanceRepository(projectId)
571
-
572
- const [library, project, compositeInstances, states] = await Promise.all([
573
- libraryBackend.loadLibrary(projectInfo.libraryId, signal),
574
- projectBackend.getProject(projectId, signal),
575
- compositeInstanceRepo.getAllItems(),
576
- instanceStateService.getInstanceStates(projectId),
577
- ])
578
-
579
- const workset = new OperationWorkset(
580
- projectId,
581
- operation,
582
- library,
583
- projectInfo.libraryId,
584
- instanceLockService,
585
- instanceStateService,
586
- logger.child({ operationId: operation.id, service: "OperationWorkset" }),
587
- )
255
+ getNextStableInstanceStatus(instanceId: InstanceId): InstanceStatus {
256
+ const state = this.context.getState(instanceId)
588
257
 
589
- // prepare instances
590
- for (const instance of project.instances) {
591
- workset.addInstance(instance)
258
+ switch (this.currentPhase) {
259
+ case "update":
260
+ return "deployed"
261
+ case "destroy":
262
+ return "undeployed"
263
+ case "refresh":
264
+ return state.status // do not change instance status when refreshing
592
265
  }
266
+ }
593
267
 
594
- for (const instance of compositeInstances) {
595
- const worksetInstance = workset.instanceMap.get(instance.instance.id)
596
-
597
- if (worksetInstance) {
598
- worksetInstance.outputs = instance.instance.outputs
599
- worksetInstance.resolvedOutputs = instance.instance.resolvedOutputs
600
- } else if (instance.instance.parentId) {
601
- workset.addInstance(instance.instance)
602
- } else {
603
- workset.logger.warn(
604
- `ignoring instance "${instance.instance.id}" from composite instance state because it is not in the project or is not a part of known composite instance`,
605
- )
606
- continue
607
- }
608
-
609
- for (const child of instance.children) {
610
- if (!workset.instanceMap.has(child.id)) {
611
- workset.addInstance(child)
612
- }
613
- }
268
+ setupAbortControllersForAllInstances(): void {
269
+ for (const instanceId of this.allAffectedInstanceIds) {
270
+ this.setupInstanceAbortControllers(instanceId)
614
271
  }
272
+ }
615
273
 
616
- const unitSources = await libraryBackend.getResolvedUnitSources(
617
- projectInfo.libraryId,
618
- unique(Array.from(workset.instanceMap.values()).map(i => i.type)),
619
- )
620
-
621
- for (const unitSource of unitSources) {
622
- workset.unitSourceHashMap.set(unitSource.unitType, unitSource.sourceHash)
274
+ private setupInstanceAbortControllers(instanceId: InstanceId): AbortControllerPair {
275
+ const existingPair = this.instanceAbortControllers.get(instanceId)
276
+ if (existingPair) {
277
+ return existingPair
623
278
  }
624
279
 
625
- for (const state of states) {
626
- if (!workset.instanceMap.has(state.id)) {
627
- workset.logger.warn(
628
- `ignoring instance "${state.id}" from state because it is not in the project or is not a part of a composite instance`,
629
- )
630
- continue
631
- }
280
+ // create abort controllers and setup them to abort when the operation is aborted
281
+ const abortController = new AbortController()
282
+ this.abortController.signal.addEventListener("abort", () => abortController.abort())
632
283
 
633
- workset.setState(state)
634
- }
284
+ abortController.signal.addEventListener("abort", () => {
285
+ // notify frontend that the instance is being cancelled
286
+ this.updateState(instanceId, { operationState: { status: "cancelling" } })
287
+ })
635
288
 
636
- // prepare input resolver
637
- for (const instance of workset.instanceMap.values()) {
638
- workset.inputResolverNodes.set(`instance:${instance.id}`, {
639
- kind: "instance",
640
- instance,
641
- component: library.components[instance.type],
642
- })
643
- }
289
+ const forceAbortController = new AbortController()
290
+ this.forceAbortController.signal.addEventListener("abort", () => forceAbortController.abort())
644
291
 
645
- for (const hub of project.hubs) {
646
- workset.inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub })
647
- }
292
+ const pair: AbortControllerPair = { abortController, forceAbortController }
293
+ this.instanceAbortControllers.set(instanceId, pair)
648
294
 
649
- workset.inputResolver = new InputResolver(workset.inputResolverNodes, logger)
650
- workset.inputResolver.addAllNodesToWorkset()
295
+ // abort if the parent instance is cancelled
296
+ const children = this.context.getInstanceChildren(instanceId)
297
+ for (const child of children) {
298
+ const childPair = this.setupInstanceAbortControllers(child.id)
651
299
 
652
- await workset.inputResolver.process()
653
-
654
- // resolve inputs for all instances and pass outputs to input hash resolver
655
- for (const instance of workset.instanceMap.values()) {
656
- const output = workset.inputResolver.requireOutput(`instance:${instance.id}`)
657
- if (output.kind !== "instance") {
658
- throw new Error("Unexpected output kind")
659
- }
300
+ abortController.signal.addEventListener("abort", () => childPair.abortController.abort())
660
301
 
661
- workset.resolvedInstanceInputs.set(instance.id, output.resolvedInputs)
302
+ forceAbortController.signal.addEventListener("abort", () =>
303
+ childPair.forceAbortController.abort(),
304
+ )
305
+ }
662
306
 
663
- const component = workset.library.components[instance.type]
307
+ return pair
308
+ }
664
309
 
665
- workset.inputHashNodes.set(instance.id, {
666
- instance,
667
- component,
668
- resolvedInputs: output.resolvedInputs,
669
- state: workset.stateMap.get(instance.id),
670
- sourceHash: workset.getSourceHashIfApplicable(instance, component),
671
- })
310
+ cancelInstance(instanceId: InstanceId, allowForceAbort = true): void {
311
+ const abortControllerPair = this.instanceAbortControllers.get(instanceId)
312
+ if (!abortControllerPair) {
313
+ throw new Error(`No abort controller found for instance "${instanceId}"`)
672
314
  }
673
315
 
674
- // prepare input hash resolver
675
- workset.inputHashResolver = new InputHashResolver(workset.inputHashNodes, logger)
676
- workset.inputHashResolver.addAllNodesToWorkset()
316
+ const { abortController, forceAbortController } = abortControllerPair
677
317
 
678
- await workset.inputHashResolver.process()
318
+ // first try to cancel the operation gracefully
679
319
 
680
- if (operation.type !== "destroy") {
681
- await workset.calculateInstanceIdsToUpdate()
320
+ if (!abortController.signal.aborted) {
321
+ abortController.abort()
322
+ return
682
323
  }
683
324
 
684
- workset.calculateInstanceIdsToDestroy()
325
+ if (!allowForceAbort) {
326
+ return
327
+ }
685
328
 
686
- for (const instanceId of workset.instanceIdsToUpdate) {
687
- workset.instanceIdsToLockIds.add(instanceId)
329
+ // then try to force cancel the operation
330
+ if (!forceAbortController.signal.aborted) {
331
+ forceAbortController.abort()
332
+ return
688
333
  }
334
+ }
689
335
 
690
- for (const instanceId of workset.instanceIdsToDestroy) {
691
- workset.instanceIdsToLockIds.add(instanceId)
336
+ cancel(): void {
337
+ if (!this.abortController.signal.aborted) {
338
+ this.abortController.abort()
339
+ return
692
340
  }
693
341
 
694
- return workset
342
+ // then try to force cancel the operation
343
+ if (!this.forceAbortController.signal.aborted) {
344
+ this.forceAbortController.abort()
345
+ return
346
+ }
695
347
  }
696
348
  }