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