@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,84 +1,71 @@
1
- import type { LibraryBackend } from "../library"
2
- import type { StateManager } from "../state"
3
- import type { ProjectBackend } from "../project"
4
- import type {
5
- RunnerArtifact,
6
- RunnerBackend,
7
- TypedUnitStateUpdate,
8
- UnitStateUpdate,
9
- } from "../runner"
10
1
  import type { Logger } from "pino"
2
+ import type { ArtifactService } from "../artifact"
11
3
  import type {
12
4
  InstanceLockService,
5
+ InstanceStatePatch,
13
6
  InstanceStateService,
14
7
  OperationService,
8
+ ProjectModelService,
15
9
  SecretService,
10
+ UnitExtraService,
16
11
  } from "../business"
17
- import type { PubSubManager } from "../pubsub"
18
- import { randomBytes } from "node:crypto"
19
- import { v7 as uuidv7 } from "uuid"
12
+ import type { Operation, OperationUpdateInput, Project } from "../database"
13
+ import type { LibraryBackend } from "../library"
14
+ import type { RunnerBackend, TypedUnitStateUpdate, UnitStateUpdate } from "../runner"
20
15
  import {
21
- isUnitModel,
22
- parseInstanceId,
23
- type ComponentModel,
16
+ type InstanceId,
24
17
  type InstanceModel,
18
+ parseInstanceId,
19
+ type TriggerInvocation,
20
+ type UnitConfig,
21
+ type VersionedName,
25
22
  } from "@highstate/contract"
26
- import { mapValues, unique } from "remeda"
27
- import { ArtifactService } from "../artifact"
23
+ import { createId } from "@paralleldrive/cuid2"
24
+ import { mapValues } from "remeda"
25
+ import { AbortError, errorToString, isAbortErrorLike, waitForAbort } from "../common"
28
26
  import {
29
27
  type InstanceState,
30
- type UnitPage,
31
- type Operation,
32
- type Page,
33
- type Trigger,
34
- type UnitTrigger,
35
- type UnitTerminal,
36
- type Terminal,
37
- type TerminalSpec,
38
- type TriggerInvocation,
28
+ isTransientInstanceOperationStatus,
29
+ type OperationPhase,
39
30
  PromiseTracker,
40
- type InstanceStatePatch,
41
- type PageBlock,
31
+ waitAll,
42
32
  } from "../shared"
43
- import {
44
- AbortError,
45
- errorToString,
46
- isAbortErrorLike,
47
- PerformanceLogger,
48
- stringToValue,
49
- valueToString,
50
- waitForAbort,
51
- } from "../common"
52
- import { OperationWorkset, type OperationPhase } from "./operation-workset"
33
+ import { OperationContext } from "./operation-context"
34
+ import { createOperationPlan } from "./operation-plan"
35
+ import { OperationWorkset } from "./operation-workset"
53
36
 
54
37
  export class RuntimeOperation {
55
- private readonly abortController = new AbortController()
56
- private readonly forceAbortController = new AbortController()
57
-
58
- private readonly instanceAbortControllers = new Map<string, AbortController>()
59
- private readonly instanceForceAbortControllers = new Map<string, AbortController>()
60
-
61
- private readonly instancePromiseMap = new Map<string, Promise<void>>()
38
+ private readonly instancePromiseMap = new Map<InstanceId, Promise<void>>()
62
39
  private readonly promiseTracker = new PromiseTracker()
63
40
 
64
41
  private workset!: OperationWorkset
42
+ private context!: OperationContext
43
+
44
+ private readonly unlockToken = createId()
65
45
 
66
46
  constructor(
67
- private readonly projectId: string,
47
+ private readonly project: Project,
68
48
  private readonly operation: Operation,
69
49
  private readonly runnerBackend: RunnerBackend,
70
50
  private readonly libraryBackend: LibraryBackend,
71
- private readonly projectBackend: ProjectBackend,
72
51
  private readonly artifactService: ArtifactService,
73
- private readonly stateManager: StateManager,
74
52
  private readonly instanceLockService: InstanceLockService,
75
53
  private readonly operationService: OperationService,
76
54
  private readonly secretService: SecretService,
77
55
  private readonly instanceStateService: InstanceStateService,
78
- private readonly pubsubManager: PubSubManager,
56
+ private readonly projectModelService: ProjectModelService,
57
+ private readonly unitExtraService: UnitExtraService,
79
58
  private readonly logger: Logger,
80
59
  ) {}
81
60
 
61
+ cancel(): void {
62
+ this.workset.cancel()
63
+ }
64
+
65
+ cancelInstance(instanceId: InstanceId): void {
66
+ this.workset.cancelInstance(instanceId)
67
+ }
68
+
82
69
  async operateSafe(): Promise<void> {
83
70
  try {
84
71
  await this.operate()
@@ -88,21 +75,20 @@ export class RuntimeOperation {
88
75
  } catch (error) {
89
76
  if (isAbortErrorLike(error)) {
90
77
  this.logger.info("the operation was cancelled")
91
- this.operation.status = "cancelled"
92
- this.operation.error = null
93
78
 
94
- await this.operationService.updateOperation(this.projectId, this.operation)
79
+ await this.updateOperation({ status: "cancelled" })
95
80
  return
96
81
  }
97
82
 
98
83
  this.logger.error({ error }, "an error occurred while running the operation")
99
84
 
100
- this.operation.status = "failed"
101
- this.operation.error = errorToString(error)
102
-
103
- await this.operationService.updateOperation(this.projectId, this.operation)
85
+ await this.updateOperation({ status: "failed" })
86
+ await this.writeOperationLog(errorToString(error))
104
87
  } finally {
105
88
  try {
89
+ this.promiseTracker.track(this.ensureInstancesUnlocked())
90
+ this.promiseTracker.track(this.ensureOperationStatesFinalized())
91
+
106
92
  // ensure that all promises are resolved even if the operation failed
107
93
  await this.promiseTracker.waitForAll()
108
94
  } catch (error) {
@@ -117,261 +103,181 @@ export class RuntimeOperation {
117
103
  private async operate(): Promise<void> {
118
104
  this.logger.info("starting operation")
119
105
 
120
- // create the workset for the operation
121
- this.workset = await OperationWorkset.load(
122
- this.projectId,
123
- this.operation,
124
- this.projectBackend,
106
+ // 1. create the context for the operation
107
+ this.context = await OperationContext.load(
108
+ this.project.id,
125
109
  this.libraryBackend,
126
- this.stateManager,
127
- this.instanceLockService,
128
110
  this.instanceStateService,
111
+ this.projectModelService,
129
112
  this.logger,
130
- this.abortController.signal,
131
113
  )
132
114
 
133
- // start the process to lock more instances when they become available
134
- const lockAbortSignal = new AbortController()
135
- this.promiseTracker.track(this.tryLockMore(lockAbortSignal.signal))
136
-
137
- // try to lock as many instances as possible
138
- await this.workset.tryLock()
115
+ // 2. create the plan for the operation (or use provided one)
116
+ let plan: OperationPhase[]
117
+ if (this.operation.phases) {
118
+ plan = this.operation.phases
119
+ } else {
120
+ plan = createOperationPlan(
121
+ this.context,
122
+ this.operation.type,
123
+ this.operation.requestedInstanceIds,
124
+ this.operation.options,
125
+ )
139
126
 
140
- if (this.workset.instanceIdsToLockIds.size === 0) {
141
- this.logger.debug("successfully locked all instances for the operation before starting")
142
- lockAbortSignal.abort()
127
+ // persist the generated plan
128
+ await this.updateOperation({ phases: plan })
143
129
  }
144
130
 
145
- // setup abort controllers for the instances
146
- for (const instanceId of this.workset.getAllAffectedInstanceIds()) {
147
- this.setupInstanceAbortControllers(instanceId)
148
- }
131
+ // 3. create the workset to track the state of the operation
132
+ this.workset = new OperationWorkset(
133
+ this.project,
134
+ this.operation.id,
135
+ plan,
136
+ this.context,
137
+ this.instanceStateService,
138
+ )
149
139
 
150
- try {
151
- // run the operation
152
- await this.processOperation()
153
- } finally {
154
- lockAbortSignal.abort()
140
+ // 4. create operation states in datbase for all affected instances in all phases
141
+ await this.workset.setupOperationStates()
155
142
 
156
- if (this.operation.type === "preview") {
157
- // stream initial states for preview operations
158
- await this.workset.restoreAffectedInitialStates()
159
- }
160
- }
161
- }
143
+ // 5. setup abort controllers for all affected instances in all phases
144
+ this.workset.setupAbortControllersForAllInstances()
162
145
 
163
- private setupInstanceAbortControllers(instanceId: string): void {
164
- const abortController = new AbortController()
165
- this.abortController.signal.addEventListener("abort", () => abortController.abort())
166
- this.instanceAbortControllers.set(instanceId, abortController)
146
+ // 6. progressively lock instances and launch them as they get locked
147
+ this.launchLockAcquisitionSequence()
167
148
 
168
- const forceAbortController = new AbortController()
169
- this.forceAbortController.signal.addEventListener("abort", () => forceAbortController.abort())
170
- this.instanceForceAbortControllers.set(instanceId, forceAbortController)
149
+ // 7. run the operation
150
+ await this.processOperation()
171
151
  }
172
152
 
173
- private async tryLockMore(signal: AbortSignal): Promise<void> {
174
- const eventStream = this.pubsubManager.subscribe(["instance-lock", this.projectId], signal)
175
-
176
- for await (const event of eventStream) {
177
- if (event.type !== "unlocked") {
178
- continue
179
- }
180
-
181
- const instanceIdsToLock = event.instanceIds.filter(
182
- //
183
- instanceId => this.workset.instanceIdsToLockIds.has(instanceId),
184
- )
185
-
186
- if (instanceIdsToLock.length === 0) {
187
- try {
188
- await this.workset.tryLock(instanceIdsToLock)
189
- } catch (error) {
190
- this.logger.error({ error }, "failed to lock more instances during operation")
191
- }
192
- }
193
-
194
- if (this.workset.instanceIdsToLockIds.size === 0) {
195
- // no more instances to lock, stop listening for events
196
- this.logger.debug("successfully locked all instances for the operation")
197
- break
198
- }
199
- }
153
+ private async updateOperation(patch: OperationUpdateInput): Promise<void> {
154
+ Object.assign(this.operation, patch)
155
+ await this.operationService.updateOperation(this.project.id, this.operation.id, patch)
200
156
  }
201
157
 
202
- private async processOperation(): Promise<void> {
203
- this.operation.instanceIdsToUpdate = this.workset.operation.instanceIdsToUpdate
204
- this.operation.instanceIdsToDestroy = this.workset.operation.instanceIdsToDestroy
158
+ private async writeOperationLog(message: string): Promise<void> {
159
+ this.promiseTracker.track(
160
+ this.operationService.appendLog(this.project.id, this.operation.id, null, message),
161
+ )
162
+ }
205
163
 
206
- this.logger.info(
207
- "operation started with %s instances to update and %s to destroy",
208
- this.operation.instanceIdsToUpdate.length,
209
- this.operation.instanceIdsToDestroy.length,
164
+ private launchLockAcquisitionSequence(): void {
165
+ this.promiseTracker.track(
166
+ this.instanceLockService.lockInstances(
167
+ this.project.id,
168
+ Array.from(this.workset.allAffectedStateIds),
169
+ {
170
+ title: "Operation Lock",
171
+ description: `The instance is locked for the ${this.operation.type} operation "${this.operation.id}".`,
172
+ icon: "mdi:cog-sync",
173
+ },
174
+ async (_tx, newlyLockedStateIds) => {
175
+ // trigger all newly locked instances to start their processing
176
+ for (const stateId of newlyLockedStateIds) {
177
+ this.workset.markInstanceLocked(stateId)
178
+ }
179
+ },
180
+ true, // allow partial locks to allow independent free branches to run
181
+ this.workset.abortController.signal,
182
+ 60_000, // wait up to 60 seconds for unlock events before retrying
183
+ this.unlockToken,
184
+ ),
210
185
  )
186
+ }
211
187
 
212
- const phases = this.getOperationPhases()
188
+ private async processOperation(): Promise<void> {
213
189
  const errors: string[] = []
214
- let hasAbortError = false
215
190
 
216
- for (const phase of phases) {
217
- this.workset.currentPhase = phase
191
+ while (this.workset.hasRemainingPhases()) {
192
+ this.workset.nextPhase()
193
+ this.instancePromiseMap.clear()
218
194
 
219
- const promises: Promise<void>[] = []
220
- for (const instanceId of this.workset.getAffectedInstanceIds()) {
221
- const parentId = this.workset.getParentId(instanceId)
222
- if (parentId && this.workset.isAffected(parentId)) {
223
- // do not call the operation for child instances of affected composites,
224
- // they will be called by their parent instance
225
- continue
226
- }
195
+ // lauch all instances in this phase
196
+ for (const instanceId of this.workset.phaseAffectedInstanceIds) {
197
+ const instance = this.context.getInstance(instanceId)
198
+ const state = this.context.getState(instanceId)
199
+ const promise = this.getInstancePromiseForOperation(instance, state)
227
200
 
228
- promises.push(this.getInstancePromiseForOperation(instanceId))
201
+ this.instancePromiseMap.set(instanceId, promise)
202
+ this.promiseTracker.track(promise)
229
203
  }
230
204
 
231
- this.logger.info(`all operations for phase "%s" started`, phase)
232
- this.operation.status = "running"
233
- await this.operationService.updateOperation(this.projectId, this.operation)
234
-
235
- const phaseResults = await Promise.allSettled(promises)
236
- for (const result of phaseResults) {
237
- if (result.status !== "rejected") {
238
- continue
239
- }
240
-
241
- if (isAbortErrorLike(result.reason)) {
242
- hasAbortError = true
243
- } else {
244
- errors.push(errorToString(result.reason))
245
- }
246
- }
205
+ // wait for all instances in this phase to complete
206
+ await this.promiseTracker.waitForAll()
247
207
 
248
- this.logger.info(`all operations for phase "%s" completed`, phase)
208
+ this.logger.info(`phase "%s" completed`, this.workset.currentPhase)
249
209
  }
250
210
 
251
211
  if (errors.length > 0) {
252
212
  this.operation.status = "failed"
253
- // TODO: map errors to instances inside operation
254
- this.operation.error = errors.join("\n\n")
255
- } else if (hasAbortError) {
256
- this.operation.status = "cancelled"
257
- this.operation.error = null
213
+
214
+ this.operationService.appendLog(
215
+ this.project.id,
216
+ this.operation.id,
217
+ null,
218
+ `Operation failed with the following errors:\n${errors.join("\n")}`,
219
+ )
258
220
  } else {
259
221
  this.operation.status = "completed"
260
- this.operation.error = null
261
222
  }
262
223
 
263
- await this.operationService.updateOperation(this.projectId, this.operation)
224
+ await this.operationService.markOperationFinished(
225
+ this.project.id,
226
+ this.operation.id,
227
+ this.operation.status,
228
+ )
264
229
 
265
230
  this.logger.info("operation completed")
266
231
  }
267
232
 
268
- cancelInstance(instanceId: string): void {
269
- const abortController = this.instanceAbortControllers.get(instanceId)
270
- if (!abortController) {
271
- throw new Error(`No abort controller found for instance "${instanceId}"`)
272
- }
273
-
274
- if (!abortController.signal.aborted) {
275
- this.logger.info(`cancelling operation for instance "${instanceId}"`)
276
- abortController.abort()
277
-
278
- // just the UX feature to indicate that we are trying to cancel the operation
279
- this.promiseTracker.track(
280
- this.workset.patchState({
281
- id: instanceId,
282
- operationStatus: {
283
- status: "cancelling",
284
- },
285
- }),
286
- )
287
-
288
- return
289
- }
290
-
291
- // then try to force cancel the operation
292
- const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
293
- if (!forceAbortController) {
294
- throw new Error(`No force abort controller found for instance "${instanceId}"`)
295
- }
296
-
297
- if (!forceAbortController.signal.aborted) {
298
- this.logger.info(`force cancelling operation for instance "${instanceId}"`)
299
- forceAbortController.abort()
300
- return
301
- }
302
-
303
- this.logger.warn(`operation for instance "${instanceId}" is already force cancelled`)
304
- }
305
-
306
- cancel(): void {
307
- if (!this.abortController.signal.aborted) {
308
- this.logger.info("cancelling the operation")
309
- this.abortController.abort()
310
- return
311
- }
312
-
313
- // then try to force cancel the operation
314
- if (!this.forceAbortController.signal.aborted) {
315
- this.logger.info("force cancelling the operation")
316
- this.forceAbortController.abort()
317
- return
318
- }
319
-
320
- this.logger.warn("the operation is already cancelled or force cancelled")
321
- }
322
-
323
- private getInstancePromiseForOperation(instanceId: string): Promise<void> {
324
- const [instanceType] = parseInstanceId(instanceId)
325
- const component = this.workset.library.components[instanceType]
326
-
327
- if (isUnitModel(component)) {
328
- return this.getUnitPromise(instanceId)
233
+ private getInstancePromiseForOperation(
234
+ instance: InstanceModel,
235
+ state: InstanceState,
236
+ ): Promise<void> {
237
+ if (instance.kind === "unit") {
238
+ return this.getUnitPromise(instance, state)
329
239
  }
330
240
 
331
- return this.getCompositePromise(instanceId)
332
- }
333
-
334
- private getOperationPhases(): OperationPhase[] {
335
- switch (this.operation.type) {
336
- case "update": {
337
- if (this.operation.instanceIdsToDestroy.length > 0) {
338
- return ["destroy", "update"]
339
- }
340
-
341
- return ["update"]
342
- }
343
- case "preview":
344
- return ["update"]
345
- case "recreate":
346
- return ["destroy", "update"]
347
- case "destroy":
348
- return ["destroy"]
349
- case "refresh":
350
- return ["refresh"]
351
- }
241
+ return this.getCompositePromise(instance)
352
242
  }
353
243
 
354
- private async getUnitPromise(instanceId: string): Promise<void> {
244
+ private getUnitPromise(instance: InstanceModel, state: InstanceState): Promise<void> {
355
245
  switch (this.workset.currentPhase) {
356
246
  case "update": {
357
- return this.updateUnit(instanceId)
247
+ return this.updateUnit(instance, state)
358
248
  }
359
249
  case "destroy": {
360
- return this.destroyUnit(instanceId)
250
+ return this.destroyUnit(instance, state)
361
251
  }
362
252
  case "refresh": {
363
- return this.refreshUnit(instanceId)
253
+ return this.refreshUnit(instance, state)
364
254
  }
365
255
  }
366
256
  }
367
257
 
368
- private getCompositePromise(instanceId: string): Promise<void> {
369
- return this.getInstancePromise(instanceId, async logger => {
370
- const instance = this.workset.getInstance(instanceId)
258
+ private getCompositePromise(instance: InstanceModel): Promise<void> {
259
+ return this.getInstancePromise(instance.id, async logger => {
260
+ let instanceState: InstanceStatePatch | undefined
371
261
 
372
- await this.setInitialOperationStatus(instance)
262
+ // set parent ID on update operation
263
+ if (this.workset.currentPhase === "update") {
264
+ if (instance.parentId) {
265
+ const parentState = this.context.getState(instance.parentId)
266
+ instanceState = { parentId: parentState.id }
267
+ } else {
268
+ instanceState = { parentId: null }
269
+ }
270
+ }
271
+
272
+ await this.workset.updateState(instance.id, {
273
+ operationState: {
274
+ status: this.workset.getTransientStatusByOperationPhase(),
275
+ startedAt: new Date(),
276
+ },
277
+ instanceState,
278
+ })
373
279
 
374
- const children = this.workset.getAffectedCompositeChildren(instanceId)
280
+ const children = this.workset.getAffectedCompositeChildren(instance.id)
375
281
  const childPromises: Promise<void>[] = []
376
282
 
377
283
  if (children.length) {
@@ -381,65 +287,37 @@ export class RuntimeOperation {
381
287
  }
382
288
 
383
289
  for (const child of children) {
384
- if (!this.operation.options.forceUpdateChildren && !this.workset.isAffected(child.id)) {
385
- // skip children that are not affected by the operation
386
- continue
387
- }
290
+ logger.debug(`waiting for child "%s"`, child)
388
291
 
389
- logger.debug(`waiting for child "%s"`, child.id)
390
- childPromises.push(this.getInstancePromiseForOperation(child.id))
391
- }
392
-
393
- try {
394
- await Promise.all(childPromises)
292
+ const instance = this.context.getInstance(child)
293
+ const state = this.context.getState(child)
294
+ const promise = this.getInstancePromiseForOperation(instance, state)
395
295
 
396
- if (children.length > 0) {
397
- logger.debug("all children completed")
398
- }
296
+ childPromises.push(promise)
297
+ }
399
298
 
400
- if (this.operation.type === "destroy") {
401
- await this.clearOperationState(instanceId)
402
- return
403
- }
299
+ await waitAll(childPromises)
404
300
 
405
- const { inputHash, dependencyOutputHash } =
406
- await this.workset.getUpToDateInputHashOutput(instance)
301
+ logger.debug("all children completed")
407
302
 
408
- await this.workset.patchState({
409
- id: instanceId,
410
- operationStatus: {
411
- status: "created",
412
- inputHash,
413
- dependencyOutputHash,
303
+ // update the instance and operation state after all children are completed in last phase
304
+ if (this.workset.isLastPhaseForInstance(instance.id)) {
305
+ await this.workset.updateState(instance.id, {
306
+ operationState: {
307
+ status: this.workset.getStableStatusByOperationPhase(),
308
+ finishedAt: new Date(),
414
309
  },
415
- })
416
- } catch (error) {
417
- if (isAbortErrorLike(error)) {
418
- await this.workset.restoreInitialState(instanceId)
419
- return
420
- }
421
-
422
- await this.workset.patchState({
423
- id: instanceId,
424
- operationStatus: {
425
- status: "error",
426
- message: errorToString(error),
310
+ instanceState: {
311
+ status: this.workset.getNextStableInstanceStatus(instance.id),
312
+ message: null,
427
313
  },
428
314
  })
429
315
  }
430
316
  })
431
317
  }
432
318
 
433
- private updateUnit(instanceId: string): Promise<void> {
434
- return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
435
- const instance = this.workset.getInstance(instanceId)
436
- const component = this.workset.library.components[instance.type]
437
- const perfLogger = new PerformanceLogger(logger)
438
- perfLogger.log("starting update promise for instance")
439
-
440
- await this.setInitialOperationStatus(instance)
441
- perfLogger.log("initial operation status set")
442
-
319
+ private updateUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
320
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
443
321
  await Promise.race([
444
322
  this.updateUnitDependencies(instance.id, logger),
445
323
 
@@ -447,115 +325,80 @@ export class RuntimeOperation {
447
325
  waitForAbort(signal),
448
326
  ])
449
327
 
450
- if (!signal.aborted) {
451
- perfLogger.log("dependencies updated")
452
- }
453
-
454
328
  signal.throwIfAborted()
455
329
 
456
330
  logger.info("updating unit")
457
331
 
458
- await this.workset.patchState({
459
- id: instance.id,
460
- operationStatus: {
332
+ await this.workset.updateState(instance.id, {
333
+ operationState: {
461
334
  status: "updating",
462
- currentResourceCount: 0,
463
- totalResourceCount: 0,
335
+ startedAt: new Date(),
464
336
  },
465
337
  })
466
338
 
467
- perfLogger.log("set 'updating' operation status")
468
339
  signal.throwIfAborted()
469
340
 
470
- const secrets = await this.secretService.getInstanceSecrets(this.projectId, instance.id)
341
+ const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
471
342
 
472
- perfLogger.log("secrets loaded")
473
343
  signal.throwIfAborted()
474
344
 
475
- const config = this.prepareUnitConfig(instance, component)
476
- perfLogger.log("unit config prepared")
477
-
478
- // extract artifact hashes from dependencies based on instance inputs
479
- const artifactHashes = this.collectArtifactIdsForInstance(instance)
480
- const artifacts = await this.stateManager
481
- .getArtifactHashIndexRepository(this.projectId)
482
- .getManyItems(artifactHashes)
345
+ const config = this.prepareUnitConfig(instance, secrets)
483
346
 
484
- logger.debug({ count: artifactHashes.length }, "artifact hashes collected from dependencies")
485
- perfLogger.log("artifact hashes collected")
347
+ // collect artifacts authorized for this instance
348
+ const artifactIds = this.collectArtifactIdsForInstance(instance)
349
+ const artifacts = await this.artifactService.getArtifactsByIds(this.project.id, artifactIds)
486
350
 
487
- // track artifact usage for this instance (only for non-preview operations)
488
- if (this.operation.type !== "preview" && artifactHashes.length > 0) {
489
- const state = this.workset.getState(instance.id)
490
-
491
- // update artifact metadata in the artifact manager
492
- this.promiseTracker.track(
493
- this.artifactService.updateUsage(
494
- this.projectId,
495
- { type: "instance", instanceId: instance.id },
496
- state?.extra?.usedArtifactIds ?? [],
497
- artifacts.map(artifact => artifact.id),
498
- ),
499
- )
500
-
501
- await this.workset.patchState({
502
- id: instance.id,
503
- extra: {
504
- usedArtifactIds: artifacts.map(artifact => artifact.id),
505
- },
506
- })
507
-
508
- perfLogger.log("artifact usage tracked")
509
- }
351
+ logger.debug({ count: artifactIds.length }, "artifact ids collected from dependencies")
510
352
 
511
353
  await this.runnerBackend[this.operation.type === "preview" ? "preview" : "update"]({
512
- projectId: this.projectId,
513
- libraryId: this.workset.libraryId,
354
+ projectId: this.project.id,
355
+ libraryId: this.project.libraryId,
356
+ stateId: state.id,
514
357
  instanceType: instance.type,
515
358
  instanceName: instance.name,
516
359
  config,
517
360
  refresh: this.operation.options.refresh,
518
- secrets: mapValues(secrets, value => valueToString(value)),
361
+ secrets,
519
362
  artifacts,
520
363
  signal,
521
364
  forceSignal,
522
365
  })
523
366
 
524
- perfLogger.log("unit update requested")
525
-
526
- await this.watchStateStream(instance.type, instance.name, logger)
527
- perfLogger.log("unit update completed")
367
+ await this.watchStateStream(state, instance.type, instance.name, logger)
528
368
  logger.info("unit updated")
529
369
  })
530
370
  }
531
371
 
532
- private async updateUnitDependencies(instanceId: string, logger: Logger): Promise<void> {
372
+ private async updateUnitDependencies(instanceId: InstanceId, logger: Logger): Promise<void> {
533
373
  try {
534
- const dependencies = this.getInstanceDependencyIds(instanceId)
374
+ const dependencies = this.context.getDependencies(instanceId)
535
375
  const dependencyPromises: Promise<void>[] = []
536
376
 
537
- for (const dependencyId of dependencies) {
538
- if (!this.operation.instanceIdsToUpdate.includes(dependencyId)) {
377
+ for (const dependency of dependencies) {
378
+ if (!this.workset.phaseAffectedInstanceIds.has(dependency.id)) {
539
379
  // skip dependencies that are not affected by the operation
540
380
  continue
541
381
  }
542
382
 
543
- logger.debug(`waiting for dependency "${dependencyId}"`)
544
- dependencyPromises.push(this.getInstancePromiseForOperation(dependencyId))
383
+ const state = this.context.getState(dependency.id)
384
+
385
+ logger.debug(`waiting for dependency "%s"`, dependency.id)
386
+ dependencyPromises.push(this.getInstancePromiseForOperation(dependency, state))
545
387
  }
546
388
 
547
- await Promise.all(dependencyPromises)
389
+ await waitAll(dependencyPromises)
548
390
 
549
391
  if (dependencies.length > 0) {
550
392
  logger.info("all dependencies completed")
551
393
  }
552
- } catch {
394
+ } catch (error) {
553
395
  // abort the instance if any dependency fails
554
- throw new AbortError()
396
+ throw new AbortError("One of the dependencies failed", { cause: error })
555
397
  }
556
398
  }
557
399
 
558
400
  private async processBeforeDestroyTriggers(
401
+ instance: InstanceModel,
559
402
  state: InstanceState,
560
403
  logger: Logger,
561
404
  signal: AbortSignal,
@@ -566,15 +409,7 @@ export class RuntimeOperation {
566
409
  return
567
410
  }
568
411
 
569
- const instance = this.workset.getInstance(state.id)
570
- const component = this.workset.library.components[instance.type]
571
-
572
- // fetch triggers from state backend by their IDs
573
- const triggerIds = Object.values(state.extra?.triggerIds ?? {})
574
-
575
- const allTriggers = await this.stateManager
576
- .getTriggerRepository(this.projectId)
577
- .getManyItems(triggerIds)
412
+ const allTriggers = await this.unitExtraService.getInstanceTriggers(this.project.id, state.id)
578
413
 
579
414
  const beforeDestroyTriggers = allTriggers.filter(
580
415
  trigger => trigger.spec.type === "before-destroy",
@@ -584,96 +419,73 @@ export class RuntimeOperation {
584
419
  return
585
420
  }
586
421
 
587
- // get trigger names from state mapping by finding the key for each trigger ID
588
- const invokedTriggers = beforeDestroyTriggers.map(trigger => {
589
- const triggerName = Object.keys(state?.extra?.triggerIds ?? {}).find(
590
- name => state?.extra?.triggerIds?.[name] === trigger.id,
591
- )
592
-
593
- return { name: triggerName || "unknown" }
594
- })
422
+ const invokedTriggers = beforeDestroyTriggers.map(trigger => ({
423
+ name: trigger.name,
424
+ }))
595
425
 
596
- await this.workset.patchState({
597
- id: instance.id,
598
- operationStatus: {
599
- status: "processing-triggers",
600
- },
426
+ await this.workset.updateState(instance.id, {
427
+ operationState: { status: "processing_triggers" },
601
428
  })
602
429
 
603
430
  logger.info("updating unit to process before-destroy triggers...")
604
431
 
605
- const secrets = await this.secretService.getInstanceSecrets(this.projectId, instance.id)
432
+ const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
606
433
 
607
434
  await this.runnerBackend.update({
608
- projectId: this.projectId,
609
- libraryId: this.workset.libraryId,
435
+ projectId: this.project.id,
436
+ stateId: state.id,
437
+ libraryId: this.project.libraryId,
610
438
  instanceType: instance.type,
611
439
  instanceName: instance.name,
612
- config: this.prepareUnitConfig(instance, component, invokedTriggers),
440
+ config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
613
441
  refresh: this.operation.options.refresh,
614
- secrets: mapValues(secrets, value => valueToString(value)),
442
+ secrets,
615
443
  signal,
616
444
  forceSignal,
617
445
  })
618
446
 
619
447
  logger.debug("unit update requested")
620
448
 
621
- await this.watchStateStream(instance.type, instance.name, logger)
449
+ await this.watchStateStream(state, instance.type, instance.name, logger)
622
450
  logger.debug("before-destroy triggers processed")
623
451
  }
624
452
 
625
- private async destroyUnit(instanceId: string): Promise<void> {
626
- return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
627
- await this.workset.patchState({
628
- id: instanceId,
629
- operationStatus: {
630
- status: "pending",
631
- },
632
- })
633
-
634
- signal.throwIfAborted()
635
-
636
- const state = this.workset.getState(instanceId)
637
- if (!state) {
638
- logger.warn("state not found for unit, but destroy was requested")
639
- return
640
- }
641
-
453
+ private async destroyUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
454
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
642
455
  const dependentPromises: Promise<void>[] = []
643
- const dependents = this.workset.getDependentStates(state.id)
456
+ const dependents = this.context.getDependentStates(instance.id)
644
457
 
645
458
  for (const dependent of dependents) {
646
- if (
647
- !this.operation.options.destroyDependentInstances &&
648
- !this.operation.instanceIdsToDestroy.includes(dependent.id)
649
- ) {
459
+ if (!this.workset.phaseAffectedInstanceIds.has(dependent.instanceId)) {
650
460
  // skip dependents that are not affected by the operation
651
461
  continue
652
462
  }
653
463
 
654
- dependentPromises.push(this.getInstancePromiseForOperation(dependent.id))
464
+ const instance = this.context.getInstance(dependent.instanceId)
465
+ dependentPromises.push(this.getInstancePromiseForOperation(instance, dependent))
655
466
  }
656
467
 
657
- await Promise.all(dependentPromises)
468
+ await waitAll(dependentPromises)
658
469
  signal.throwIfAborted()
659
470
 
660
- await this.processBeforeDestroyTriggers(state, logger, signal, forceSignal)
471
+ await this.processBeforeDestroyTriggers(instance, state, logger, signal, forceSignal)
661
472
  signal.throwIfAborted()
662
473
 
663
474
  logger.info("destroying unit...")
664
475
 
665
- await this.workset.patchState({
666
- id: instanceId,
667
- operationStatus: {
476
+ await this.workset.updateState(instance.id, {
477
+ operationState: {
668
478
  status: "destroying",
479
+ startedAt: new Date(),
669
480
  },
670
481
  })
671
482
 
672
- const [type, name] = parseInstanceId(instanceId)
483
+ const [type, name] = parseInstanceId(instance.id)
673
484
 
674
485
  await this.runnerBackend.destroy({
675
- projectId: this.projectId,
676
- libraryId: this.workset.libraryId,
486
+ projectId: this.project.id,
487
+ stateId: state.id,
488
+ libraryId: this.project.libraryId,
677
489
  instanceType: type,
678
490
  instanceName: name,
679
491
  refresh: this.operation.options.refresh,
@@ -685,47 +497,32 @@ export class RuntimeOperation {
685
497
 
686
498
  logger.debug("destroy request sent")
687
499
 
688
- await this.watchStateStream(type, name, logger)
500
+ await this.watchStateStream(state, type, name, logger)
689
501
 
690
502
  // clean up artifacts after successful destruction
691
- try {
692
- const artifactIds = unique([
693
- ...Object.values(state.extra?.ownedArtifactIds ?? {}).flat(),
694
- ...(state.extra?.usedArtifactIds ?? []),
695
- ])
696
-
697
- await this.artifactService.removeUsages(
698
- //
699
- this.projectId,
700
- artifactIds,
701
- [{ type: "instance", instanceId: state.id }],
702
- )
703
- } catch (error) {
704
- logger.warn({ error }, "failed to cleanup artifacts for destroyed instance")
705
- }
503
+ await this.artifactService.clearInstanceArtifactReferences(this.project.id, instance.id)
706
504
 
707
505
  logger.info("unit destroyed")
708
506
  })
709
507
  }
710
508
 
711
- private async refreshUnit(instanceId: string) {
712
- return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
713
- await this.workset.patchState({
714
- id: instanceId,
715
- operationStatus: {
509
+ private async refreshUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
510
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
511
+ await this.workset.updateState(instance.id, {
512
+ operationState: {
716
513
  status: "refreshing",
717
- currentResourceCount: 0,
718
- totalResourceCount: 0,
514
+ startedAt: new Date(),
719
515
  },
720
516
  })
721
517
 
722
518
  logger.info("refreshing unit...")
723
519
 
724
- const [type, name] = parseInstanceId(instanceId)
520
+ const [type, name] = parseInstanceId(instance.id)
725
521
 
726
522
  await this.runnerBackend.refresh({
727
- projectId: this.projectId,
728
- libraryId: this.workset.libraryId,
523
+ projectId: this.project.id,
524
+ stateId: state.id,
525
+ libraryId: this.project.libraryId,
729
526
  instanceType: type,
730
527
  instanceName: name,
731
528
  signal,
@@ -734,19 +531,21 @@ export class RuntimeOperation {
734
531
 
735
532
  logger.debug("unit refresh requested")
736
533
 
737
- await this.watchStateStream(type, name, logger)
534
+ await this.watchStateStream(state, type, name, logger)
738
535
  logger.info("unit refreshed")
739
536
  })
740
537
  }
741
538
 
742
539
  private async watchStateStream(
743
- instanceType: string,
540
+ state: InstanceState,
541
+ instanceType: VersionedName,
744
542
  instanceName: string,
745
543
  logger: Logger,
746
544
  ): Promise<void> {
747
545
  const stream = this.runnerBackend.watch({
748
- projectId: this.projectId,
749
- libraryId: this.workset.libraryId,
546
+ projectId: this.project.id,
547
+ stateId: state.id,
548
+ libraryId: this.project.libraryId,
750
549
  instanceType,
751
550
  instanceName,
752
551
  })
@@ -755,18 +554,12 @@ export class RuntimeOperation {
755
554
 
756
555
  for await (update of stream) {
757
556
  try {
758
- await this.handleUnitStateUpdate(update)
557
+ await this.handleUnitStateUpdate(update, state)
759
558
  } catch (error) {
760
559
  logger.error({ error }, "failed to handle unit state update")
761
560
  }
762
561
 
763
562
  if (update.type === "error") {
764
- if (isAbortErrorLike(update.message)) {
765
- // abort the unit if the returned error contains some abort-like pattern
766
- // generally, this might not be safe, but for now, we assume that
767
- throw new AbortError()
768
- }
769
-
770
563
  // rethrow the error to stop the execution of dependent units
771
564
  throw new Error(`An error occurred while processing the unit "${update.unitId}"`, {
772
565
  cause: update.message,
@@ -785,187 +578,155 @@ export class RuntimeOperation {
785
578
 
786
579
  private prepareUnitConfig(
787
580
  instance: InstanceModel,
788
- component: ComponentModel,
581
+ secrets: Record<string, unknown>,
789
582
  invokedTriggers: TriggerInvocation[] = [],
790
- ): Record<string, string> {
791
- const config: Record<string, string> = {}
792
-
793
- for (const key of Object.keys(component.args)) {
794
- // explicitly set empty values to remove old values in the config
795
- const value = instance.args?.[key]
796
- config[key] = value ? valueToString(value) : ""
797
- }
798
-
799
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
800
-
801
- for (const key of Object.keys(component.inputs)) {
802
- const inputs = instanceInputs[key] ?? []
583
+ ): UnitConfig {
584
+ const resolvedInputs = this.context.getResolvedInputs(instance.id)
803
585
 
804
- config[`input.${key}`] = JSON.stringify(inputs.map(input => input.input))
586
+ return {
587
+ instanceId: instance.id,
588
+ args: instance.args ?? {},
589
+ inputs: mapValues(resolvedInputs ?? {}, input => input.map(value => value.input)),
590
+ invokedTriggers,
591
+ secretNames: Object.keys(secrets),
592
+ stateIdMap: this.context.getInstanceIdToStateIdMap(),
805
593
  }
806
-
807
- config["$invokedTriggers"] = JSON.stringify(invokedTriggers)
808
-
809
- return config
810
594
  }
811
595
 
812
- private async handleUnitStateUpdate(update: UnitStateUpdate): Promise<void> {
596
+ private async handleUnitStateUpdate(
597
+ update: UnitStateUpdate,
598
+ state: InstanceState,
599
+ ): Promise<void> {
813
600
  switch (update.type) {
814
601
  case "message":
815
- this.handleUnitMessage(update)
602
+ this.handleUnitMessage(update, state)
816
603
  return
817
604
  case "progress":
818
605
  await this.handleUnitProgress(update)
819
606
  return
820
607
  case "error":
821
- await this.handleUnitError(update)
608
+ await this.handleUnitError(update, state)
822
609
  return
823
610
  case "completion":
824
- await this.handleUnitCompletion(update)
611
+ await this.handleUnitCompletion(update, state)
825
612
  return
826
613
  }
827
614
  }
828
615
 
829
- private handleUnitMessage(update: TypedUnitStateUpdate<"message">): void {
830
- // collect all instances in the hierarchy that should receive this log
831
- const instanceIdsInHierarchy: string[] = []
832
- let instance: InstanceModel | null = this.workset.getInstance(update.unitId)
833
-
616
+ private handleUnitMessage(update: TypedUnitStateUpdate<"message">, state: InstanceState): void {
617
+ // append logs to the instance and all its parents
834
618
  for (;;) {
835
- instanceIdsInHierarchy.push(instance.id)
619
+ this.promiseTracker.track(
620
+ this.operationService.appendLog(
621
+ this.project.id,
622
+ this.operation.id,
623
+ state.id,
624
+ update.message,
625
+ ),
626
+ )
836
627
 
837
- if (!instance.parentId) {
628
+ if (!state.parentInstanceId) {
838
629
  break
839
630
  }
840
631
 
841
- instance = this.workset.getInstance(instance.parentId)
632
+ state = this.context.getState(state.parentInstanceId)
842
633
  }
843
-
844
- // persist the log for all instances in the hierarchy
845
- // TODO: batch
846
- this.promiseTracker.track(
847
- this.operationService.appendLogs(this.projectId, this.operation.id, instanceIdsInHierarchy, [
848
- update.message,
849
- ]),
850
- )
851
- return
852
634
  }
853
635
 
854
636
  private async handleUnitProgress(update: TypedUnitStateUpdate<"progress">): Promise<void> {
855
- const patch: InstanceStatePatch = { id: update.unitId }
856
- patch.operationStatus = {}
857
-
858
- if (update.currentResourceCount !== undefined) {
859
- patch.operationStatus.currentResourceCount = update.currentResourceCount
860
- }
861
-
862
- if (update.totalResourceCount !== undefined) {
863
- patch.operationStatus.totalResourceCount = update.totalResourceCount
864
- }
865
-
866
- await this.workset.patchState(patch)
637
+ await this.workset.updateState(update.unitId, {
638
+ operationState: {
639
+ currentResourceCount: update.currentResourceCount,
640
+ totalResourceCount: update.totalResourceCount,
641
+ },
642
+ })
867
643
  }
868
644
 
869
- private async handleUnitError(update: TypedUnitStateUpdate<"error">): Promise<void> {
870
- if (isAbortErrorLike(update.message)) {
871
- return
872
- }
873
-
874
- const patch: InstanceStatePatch = {
875
- id: update.unitId,
876
- operationStatus: {
877
- status: "error",
645
+ private async handleUnitError(
646
+ update: TypedUnitStateUpdate<"error">,
647
+ state: InstanceState,
648
+ ): Promise<void> {
649
+ await this.workset.updateState(update.unitId, {
650
+ instanceState: {
651
+ // keep "deployed" status for initially deployed instances even if the operation was failed or cancelled
652
+ status: state.status === "deployed" ? "deployed" : "failed",
878
653
  message: update.message,
879
654
  },
880
- }
881
-
882
- await this.workset.patchState(patch)
655
+ operationState: {
656
+ status: isAbortErrorLike(update.message) ? "cancelled" : "failed",
657
+ finishedAt: new Date(),
658
+ },
659
+ })
883
660
  }
884
661
 
885
- private async handleUnitCompletion(update: TypedUnitStateUpdate<"completion">): Promise<void> {
886
- if (this.workset.currentPhase === "destroy") {
887
- // if the destroy operation was completed, clear the state from all operation-related fields
888
- await this.clearOperationState(update.unitId)
889
- return
890
- }
891
-
892
- const state = this.workset.getState(update.unitId)
893
- if (!state) {
894
- throw new Error(
895
- `Cannot handle completion for unit "${update.unitId}" because its state is not found.`,
896
- )
897
- }
898
-
899
- const instance = this.workset.getInstance(update.unitId)
662
+ private async handleUnitCompletion(
663
+ update: TypedUnitStateUpdate<"completion">,
664
+ state: InstanceState,
665
+ ): Promise<void> {
666
+ const instance = this.context.getInstance(update.unitId)
900
667
 
901
- // patch the state with output hash first to recalculate the input hash
902
- if (update.outputHash) {
903
- await this.workset.patchState({
904
- id: update.unitId,
905
- operationStatus: { outputHash: update.outputHash },
906
- })
668
+ const data: InstanceStatePatch = {
669
+ status: this.workset.getNextStableInstanceStatus(instance.id),
670
+ message: update.message,
671
+ statusFields: update.statusFields ?? null,
907
672
  }
908
673
 
909
- const { inputHash, dependencyOutputHash } =
910
- await this.workset.getUpToDateInputHashOutput(instance)
911
-
912
- const patch: InstanceStatePatch = {
913
- id: update.unitId,
914
- operationStatus: {
915
- status: "created",
916
- message:
917
- update.message ?? "The operation on this instance has been completed successfully.",
918
- outputHash: update.outputHash ?? undefined,
919
- inputHash,
920
- dependencyOutputHash,
921
- },
922
- extra: {
923
- statusFields: update.statusFields,
674
+ if (update.operationType !== "destroy") {
675
+ // давайте еще больше усложним и без того сложную штуку
676
+ // set output hash before calculating input hash to capture up-to-date output hash for dependencies
677
+ state.outputHash = update.outputHash ?? null
924
678
 
925
- terminalIds: update.terminals
926
- ? this.processTerminals(update.unitId, update.terminals, state.extra?.terminalIds)
927
- : undefined,
679
+ // recalculate the input and output hashes for the instance
680
+ const { inputHash, dependencyOutputHash } =
681
+ await this.context.getUpToDateInputHashOutput(instance)
928
682
 
929
- pageIds: update.pages
930
- ? this.processPages(update.unitId, update.pages, state.extra?.pageIds)
931
- : undefined,
683
+ data.inputHash = inputHash
684
+ data.dependencyOutputHash = dependencyOutputHash
685
+ data.outputHash = update.outputHash
932
686
 
933
- triggerIds: update.triggers
934
- ? this.processTriggers(update.unitId, update.triggers, state.extra?.triggerIds)
935
- : undefined,
936
-
937
- ownedArtifactIds: update.artifacts
938
- ? this.processArtifacts(update.unitId, update.artifacts, state.extra?.ownedArtifactIds)
939
- : undefined,
687
+ // also update the parent ID
688
+ if (instance.parentId) {
689
+ const parentState = this.context.getState(instance.parentId)
690
+ data.parentId = parentState.id
691
+ } else {
692
+ data.parentId = null
693
+ }
694
+ } else {
695
+ data.message = null
696
+ data.inputHash = null
697
+ data.dependencyOutputHash = null
698
+ data.outputHash = null
699
+ data.parentId = null
700
+ }
701
+
702
+ // update the operation state
703
+ await this.workset.updateState(instance.id, {
704
+ // TODO: honestly, it is not correct
705
+ // may be we should track operation phases separately
706
+ // or introduce status like "destroyed-before-recreation" (quite ugly though)
707
+ operationState: {
708
+ status: this.workset.getStableStatusByOperationPhase(),
709
+ finishedAt: new Date(),
940
710
  },
941
- }
942
-
943
- if (update.secrets) {
944
- const instance = this.workset.getInstance(update.unitId)
945
-
946
- this.promiseTracker.track(
947
- this.updateInstanceSecrets(
948
- instance,
949
- mapValues(update.secrets, value => stringToValue(value)),
950
- ),
951
- )
952
- }
953
711
 
954
- await this.workset.patchState(patch)
955
- }
956
-
957
- private async clearOperationState(instanceId: string): Promise<void> {
958
- const patch: InstanceStatePatch = {
959
- id: instanceId,
960
- operationStatus: null,
961
- extra: null,
962
- }
963
-
964
- await this.workset.patchState(patch)
712
+ // do not write instance state for non-last phases of the instance
713
+ instanceState: this.workset.isLastPhaseForInstance(instance.id) ? data : undefined,
714
+
715
+ // also do not write unit extra data for non-last phases of the instance
716
+ unitExtra: this.workset.isLastPhaseForInstance(instance.id)
717
+ ? {
718
+ pages: update.pages ?? [],
719
+ terminals: update.terminals ?? [],
720
+ triggers: update.triggers ?? [],
721
+ workers: update.workers ?? [],
722
+ secrets: update.secrets ?? {},
723
+ }
724
+ : undefined,
725
+ })
965
726
  }
966
727
 
967
728
  private getInstancePromise(
968
- instanceId: string,
729
+ instanceId: InstanceId,
969
730
  fn: (logger: Logger, signal: AbortSignal, forceSignal: AbortSignal) => Promise<void>,
970
731
  ): Promise<void> {
971
732
  let instancePromise = this.instancePromiseMap.get(instanceId)
@@ -973,30 +734,54 @@ export class RuntimeOperation {
973
734
  return instancePromise
974
735
  }
975
736
 
976
- const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
977
- const abortController = this.instanceAbortControllers.get(instanceId)
978
- const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
737
+ // "pending" -> "running" if at least one instance is running
738
+ if (this.operation.status === "pending") {
739
+ this.operation.status = "running"
740
+ this.promiseTracker.track(this.updateOperation({ status: this.operation.status }))
741
+ }
742
+
743
+ const state = this.context.getState(instanceId)
979
744
 
980
- if (!abortController || !forceAbortController) {
745
+ const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
746
+ const abortControllerPair = this.workset.instanceAbortControllers.get(instanceId)
747
+ if (!abortControllerPair) {
981
748
  throw new Error(`Abort controllers for instance "${instanceId}" are not initialized`)
982
749
  }
983
750
 
751
+ const { abortController, forceAbortController } = abortControllerPair
752
+
984
753
  instancePromise = this.workset
985
- .ensureInstanceLocked(instanceId, abortController.signal)
754
+ .waitForInstanceLock(state.id, abortController.signal)
986
755
  .then(() => fn(logger, abortController.signal, forceAbortController.signal))
987
756
  .catch(error => {
988
- if (isAbortErrorLike(error)) {
989
- // if the operation was aborted, restore the initial state of the instance
990
- this.promiseTracker.track(this.workset.restoreInitialState(instanceId))
991
-
992
- throw error
993
- }
994
-
995
757
  if (this.operation.status !== "failing") {
996
758
  // report the failing status of the operation
997
759
  this.operation.status = "failing"
760
+ this.promiseTracker.track(this.updateOperation({ status: this.operation.status }))
761
+ }
762
+
763
+ if (isTransientInstanceOperationStatus(state.lastOperationState?.status)) {
764
+ // if the underlying method did not correctly update the instance status, do it here
765
+ this.promiseTracker.track(
766
+ this.workset.updateState(instanceId, {
767
+ operationState: {
768
+ status: isAbortErrorLike(error) ? "cancelled" : "failed",
769
+ finishedAt: new Date(),
770
+ },
771
+ instanceState: {
772
+ // keep "deployed" status for initially deployed instances even if the operation was failed or cancelled
773
+ status: state.status === "deployed" ? "deployed" : "failed",
774
+ },
775
+ }),
776
+ )
777
+
998
778
  this.promiseTracker.track(
999
- this.operationService.updateOperation(this.projectId, this.operation),
779
+ this.operationService.appendLog(
780
+ this.project.id,
781
+ this.operation.id,
782
+ state.id,
783
+ errorToString(error),
784
+ ),
1000
785
  )
1001
786
  }
1002
787
 
@@ -1004,48 +789,67 @@ export class RuntimeOperation {
1004
789
  throw error
1005
790
  })
1006
791
  .finally(() => {
792
+ if (!this.workset.isLastPhaseForInstance(instanceId)) {
793
+ // do not finalize the instance if it has more phases to run
794
+ return
795
+ }
796
+
1007
797
  this.instancePromiseMap.delete(instanceId)
1008
798
 
1009
799
  // TODO: ideally we should defer unlocking until all direct dependents are completed,
1010
800
  // to ensure that they are received expected inputs from this instance
1011
801
  this.promiseTracker.track(
1012
- this.instanceLockService.unlockInstancesUnconditionally(this.projectId, [instanceId]),
802
+ this.instanceLockService
803
+ .unlockInstances(this.project.id, [state.id], this.unlockToken)
804
+ .then(() => this.workset.markInstanceUnlocked(state.id)),
1013
805
  )
806
+
807
+ this.logger.debug(`promise for instance "%s" completed`, instanceId)
1014
808
  })
1015
809
 
1016
810
  this.instancePromiseMap.set(instanceId, instancePromise)
811
+ this.logger.trace(`created new promise for instance "%s"`, instanceId)
1017
812
 
1018
813
  return instancePromise
1019
814
  }
1020
815
 
1021
- private async setInitialOperationStatus(instance: InstanceModel): Promise<void> {
1022
- const state = this.workset.getState(instance.id)
1023
-
1024
- await this.workset.patchState({
1025
- id: instance.id,
1026
- parentId: instance.parentId,
1027
- operationStatus: {
1028
- status: "pending",
1029
- message: "",
1030
- dependencyIds: this.getInstanceDependencyIds(instance.id),
1031
- latestOperationId: this.operation.id,
1032
- inputHashNonce: state?.operationStatus?.inputHashNonce ?? randomBytes(4).readInt32LE(),
1033
- args: instance.args,
1034
- },
1035
- })
1036
- }
816
+ private async ensureInstancesUnlocked(): Promise<void> {
817
+ const lockedStateIds = Array.from(this.workset.getLockedStateIds())
818
+ if (lockedStateIds.length === 0) {
819
+ return
820
+ }
1037
821
 
1038
- private getInstanceDependencyIds(instanceId: string): string[] {
1039
- const dependencies = new Set<string>()
1040
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instanceId) ?? {}
822
+ this.logger.warn("unlocking %d locked instances before shutting down", lockedStateIds.length)
1041
823
 
1042
- for (const inputs of Object.values(instanceInputs)) {
1043
- for (const input of inputs) {
1044
- dependencies.add(input.input.instanceId)
1045
- }
824
+ await this.instanceLockService.unlockInstances(
825
+ this.project.id,
826
+ lockedStateIds,
827
+ this.unlockToken,
828
+ )
829
+ }
830
+
831
+ private async ensureOperationStatesFinalized(): Promise<void> {
832
+ const unfinishedStates = this.context.getUnfinishedOperationStates()
833
+ if (unfinishedStates.length === 0) {
834
+ return
1046
835
  }
1047
836
 
1048
- return Array.from(dependencies)
837
+ this.logger.warn(
838
+ "finalizing %d unfinished operation states before shutting down",
839
+ unfinishedStates.length,
840
+ )
841
+
842
+ for (const state of unfinishedStates) {
843
+ await this.workset.updateState(state.instanceId, {
844
+ operationState: {
845
+ status: "failed",
846
+ finishedAt: new Date(),
847
+ },
848
+ instanceState: {
849
+ status: state.status === "deployed" ? "deployed" : "failed",
850
+ },
851
+ })
852
+ }
1049
853
  }
1050
854
 
1051
855
  /**
@@ -1054,22 +858,22 @@ export class RuntimeOperation {
1054
858
  */
1055
859
  private collectArtifactIdsForInstance(instance: InstanceModel): string[] {
1056
860
  const artifactIds = new Set<string>()
1057
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
861
+ const instanceInputs = this.context.getResolvedInputs(instance.id) ?? {}
1058
862
 
1059
863
  for (const inputs of Object.values(instanceInputs)) {
1060
864
  for (const input of inputs) {
1061
- const dependencyState = this.workset.getState(input.input.instanceId)
1062
- if (!dependencyState?.extra?.ownedArtifactIds) {
865
+ const dependencyState = this.context.getState(input.input.instanceId)
866
+ if (!dependencyState.exportedArtifactIds) {
1063
867
  continue
1064
868
  }
1065
869
 
1066
870
  const outputKey = input.input.output
1067
- const outputArtifacts = dependencyState.extra.ownedArtifactIds[outputKey]
1068
- if (!outputArtifacts) {
871
+ const outputArtifactIds = dependencyState.exportedArtifactIds[outputKey]
872
+ if (!outputArtifactIds) {
1069
873
  continue
1070
874
  }
1071
875
 
1072
- for (const hash of outputArtifacts) {
876
+ for (const hash of outputArtifactIds) {
1073
877
  artifactIds.add(hash)
1074
878
  }
1075
879
  }
@@ -1077,266 +881,4 @@ export class RuntimeOperation {
1077
881
 
1078
882
  return Array.from(artifactIds)
1079
883
  }
1080
-
1081
- /**
1082
- * Updates the secrets for a specific instance.
1083
- * Processes terminals from instance state update.
1084
- *
1085
- * Converts unit terminals to instance terminals and handles cleanup.
1086
- */
1087
- private processTerminals(
1088
- instanceId: string,
1089
- unitTerminals: UnitTerminal[],
1090
- currentTerminalIds: Record<string, string> | undefined,
1091
- ): Record<string, string> {
1092
- const terminals: Terminal[] = unitTerminals.map(unitTerminal => ({
1093
- id: currentTerminalIds?.[unitTerminal.name] ?? uuidv7(),
1094
- instanceId,
1095
- meta: unitTerminal.meta ?? {},
1096
- }))
1097
-
1098
- const terminalSpecEntries: [string, TerminalSpec][] = unitTerminals
1099
- //
1100
- .map((unitTerminal, index) => [terminals[index].id, unitTerminal.spec])
1101
-
1102
- // create mapping from local name to UUID
1103
- const newTerminalIds: Record<string, string> = {}
1104
- for (let i = 0; i < unitTerminals.length; i++) {
1105
- const unitTerminal = unitTerminals[i]
1106
- const instanceTerminal = terminals[i]
1107
- newTerminalIds[unitTerminal.name] = instanceTerminal.id
1108
- }
1109
-
1110
- // find terminals that need to be deleted (old terminals not in new list)
1111
- const terminalsToDelete = Object.values(currentTerminalIds ?? {}).filter(
1112
- id => !Object.values(newTerminalIds).includes(id),
1113
- )
1114
-
1115
- if (terminals.length > 0) {
1116
- this.promiseTracker.track(this.putTerminals(terminals, terminalSpecEntries))
1117
- }
1118
-
1119
- if (terminalsToDelete.length > 0) {
1120
- this.promiseTracker.track(this.deleteTerminals(terminalsToDelete))
1121
- }
1122
-
1123
- return newTerminalIds
1124
- }
1125
-
1126
- private async putTerminals(
1127
- terminals: Terminal[],
1128
- terminalSpecEntries: [string, TerminalSpec][],
1129
- ): Promise<void> {
1130
- if (this.operation.type === "preview") {
1131
- return
1132
- }
1133
-
1134
- await using batch = this.stateManager.batch()
1135
-
1136
- await Promise.all([
1137
- this.stateManager.getTerminalRepository(this.projectId).putManyItems(terminals, batch),
1138
-
1139
- this.stateManager
1140
- .getTerminalSpecRepository(this.projectId)
1141
- .putMany(terminalSpecEntries, batch),
1142
- ])
1143
-
1144
- await batch.write()
1145
- }
1146
-
1147
- private async deleteTerminals(terminalIds: string[]): Promise<void> {
1148
- if (this.operation.type === "preview") {
1149
- return
1150
- }
1151
-
1152
- await using batch = this.stateManager.batch()
1153
-
1154
- await Promise.all([
1155
- this.stateManager.getTerminalRepository(this.projectId).deleteMany(terminalIds, batch),
1156
- this.stateManager.getTerminalSpecRepository(this.projectId).deleteMany(terminalIds, batch),
1157
- ])
1158
-
1159
- await batch.write()
1160
- }
1161
-
1162
- /**
1163
- * Processes pages from instance state update.
1164
- *
1165
- * Converts unit pages to instance pages and handles cleanup.
1166
- */
1167
- private processPages(
1168
- instanceId: string,
1169
- unitPages: UnitPage[],
1170
- currentPageIds: Record<string, string> | undefined,
1171
- ): Record<string, string> {
1172
- const pages: Page[] = unitPages.map(unitPage => ({
1173
- id: currentPageIds?.[unitPage.name] ?? uuidv7(),
1174
- instanceId,
1175
- meta: unitPage.meta,
1176
- }))
1177
-
1178
- const pageContentEntries: [string, PageBlock[]][] = unitPages
1179
- //
1180
- .map((unitPage, index) => [pages[index].id, unitPage.content])
1181
-
1182
- // create mapping from local name to UUID
1183
- const newPageIds: Record<string, string> = {}
1184
- for (let i = 0; i < unitPages.length; i++) {
1185
- const unitPage = unitPages[i]
1186
- const instancePage = pages[i]
1187
- newPageIds[unitPage.name] = instancePage.id
1188
- }
1189
-
1190
- // find pages that need to be deleted (old pages not in new list)
1191
- const pagesToDelete = Object.values(currentPageIds ?? {}).filter(
1192
- id => !Object.values(newPageIds).includes(id),
1193
- )
1194
-
1195
- if (pages.length > 0) {
1196
- this.promiseTracker.track(this.putPages(pages, pageContentEntries))
1197
- }
1198
-
1199
- if (pagesToDelete.length > 0) {
1200
- this.promiseTracker.track(this.deletePages(pagesToDelete))
1201
- }
1202
-
1203
- return newPageIds
1204
- }
1205
-
1206
- private async putPages(
1207
- pages: Page[],
1208
- pageContentEntries: [string, PageBlock[]][],
1209
- ): Promise<void> {
1210
- if (this.operation.type === "preview") {
1211
- return
1212
- }
1213
-
1214
- const batch = this.stateManager.batch()
1215
-
1216
- await Promise.all([
1217
- this.stateManager.getPageRepository(this.projectId).putManyItems(pages, batch),
1218
- this.stateManager.getPageContentRepository(this.projectId).putMany(pageContentEntries, batch),
1219
- ])
1220
-
1221
- await batch.write()
1222
- }
1223
-
1224
- private async deletePages(pageIds: string[]): Promise<void> {
1225
- if (this.operation.type === "preview") {
1226
- return
1227
- }
1228
-
1229
- const batch = this.stateManager.batch()
1230
-
1231
- await Promise.all([
1232
- this.stateManager.getPageRepository(this.projectId).deleteMany(pageIds, batch),
1233
- this.stateManager.getPageContentRepository(this.projectId).deleteMany(pageIds, batch),
1234
- ])
1235
-
1236
- await batch.write()
1237
- }
1238
-
1239
- /**
1240
- * Processes triggers from instance state update.
1241
- *
1242
- * Converts unit triggers to instance triggers and handles cleanup.
1243
- */
1244
- private processTriggers(
1245
- instanceId: string,
1246
- unitTriggers: UnitTrigger[],
1247
- currentTriggerIds: Record<string, string> | undefined,
1248
- ): Record<string, string> {
1249
- const triggers: Trigger[] = unitTriggers.map(unitTrigger => ({
1250
- id: currentTriggerIds?.[unitTrigger.name] ?? uuidv7(),
1251
- instanceId,
1252
- meta: unitTrigger.meta ?? {},
1253
- spec: unitTrigger.spec,
1254
- }))
1255
-
1256
- // create mapping from local name to UUID
1257
- const newTriggerIds: Record<string, string> = {}
1258
- for (let i = 0; i < unitTriggers.length; i++) {
1259
- const unitTrigger = unitTriggers[i]
1260
- newTriggerIds[unitTrigger.name] = triggers[i].id
1261
- }
1262
-
1263
- // find triggers that need to be deleted (old triggers not in new list)
1264
- const triggersToDelete = Object.values(currentTriggerIds ?? {}).filter(
1265
- id => !Object.values(newTriggerIds).includes(id),
1266
- )
1267
-
1268
- if (triggers.length > 0) {
1269
- this.promiseTracker.track(this.putTriggers(triggers))
1270
- }
1271
-
1272
- if (triggersToDelete.length > 0) {
1273
- this.promiseTracker.track(this.deleteTriggers(triggersToDelete))
1274
- }
1275
-
1276
- return newTriggerIds
1277
- }
1278
-
1279
- private async putTriggers(triggers: Trigger[]): Promise<void> {
1280
- if (this.operation.type === "preview") {
1281
- return
1282
- }
1283
-
1284
- await this.stateManager.getTriggerRepository(this.projectId).putManyItems(triggers)
1285
- }
1286
-
1287
- private async deleteTriggers(triggerIds: string[]): Promise<void> {
1288
- if (this.operation.type === "preview") {
1289
- return
1290
- }
1291
-
1292
- await this.stateManager.getTriggerRepository(this.projectId).deleteMany(triggerIds)
1293
- }
1294
-
1295
- /**
1296
- * Processes artifacts from instance state update.
1297
- */
1298
- private processArtifacts(
1299
- instanceId: string,
1300
- newOwnedArtifacts: Record<string, RunnerArtifact[]>,
1301
- oldOwnedArtifactIds: Record<string, string[]> | undefined,
1302
- ): Record<string, string[]> {
1303
- if (this.operation.type !== "preview") {
1304
- // persist the new owned artifacts if this is not a preview operation
1305
- this.promiseTracker.track(
1306
- this.artifactService.updateUsage(
1307
- this.projectId,
1308
- { type: "instance", instanceId },
1309
- unique(Object.values(oldOwnedArtifactIds ?? {}).flat()),
1310
- unique(
1311
- Object.values(newOwnedArtifacts).flatMap(artifacts =>
1312
- artifacts.map(artifact => artifact.id),
1313
- ),
1314
- ),
1315
- ),
1316
- )
1317
- }
1318
-
1319
- return mapValues(newOwnedArtifacts, artifacts => artifacts.map(artifact => artifact.id))
1320
- }
1321
-
1322
- /**
1323
- * Updates the secrets for a specific instance.
1324
- */
1325
- private async updateInstanceSecrets(
1326
- instance: InstanceModel,
1327
- secrets: Record<string, unknown>,
1328
- ): Promise<void> {
1329
- if (this.operation.type === "preview") {
1330
- // do not update secrets in preview mode
1331
- return
1332
- }
1333
-
1334
- await this.secretService.updateInstanceSecrets(
1335
- this.projectId,
1336
- instance.id,
1337
- secrets,
1338
- [], // no secrets to delete in this context
1339
- false, // do not invalidate state even if the secrets were updated
1340
- )
1341
- }
1342
884
  }