@highstate/backend 0.9.15 → 0.9.18

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 (187) hide show
  1. package/dist/chunk-NAAIDR4U.js +8499 -0
  2. package/dist/chunk-NAAIDR4U.js.map +1 -0
  3. package/dist/chunk-OU5OQBLB.js +74 -0
  4. package/dist/chunk-OU5OQBLB.js.map +1 -0
  5. package/dist/chunk-Y7DXREVO.js +1745 -0
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +7227 -2501
  9. package/dist/index.js.map +1 -1
  10. package/dist/library/package-resolution-worker.js +7 -5
  11. package/dist/library/package-resolution-worker.js.map +1 -1
  12. package/dist/library/worker/main.js +76 -185
  13. package/dist/library/worker/main.js.map +1 -1
  14. package/dist/magic-string.es-5ABAC4JN.js +1292 -0
  15. package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
  16. package/dist/shared/index.js +3 -98
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +31 -10
  19. package/src/artifact/abstractions.ts +46 -0
  20. package/src/artifact/encryption.ts +109 -0
  21. package/src/artifact/factory.ts +36 -0
  22. package/src/artifact/index.ts +3 -0
  23. package/src/artifact/local.ts +138 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  27. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  28. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  40. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  41. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  42. package/src/business/api-key.ts +65 -0
  43. package/src/business/artifact.ts +289 -0
  44. package/src/business/backend-unlock.ts +10 -0
  45. package/src/business/index.ts +10 -0
  46. package/src/business/instance-lock.ts +125 -0
  47. package/src/business/instance-state.ts +434 -0
  48. package/src/business/operation.ts +251 -0
  49. package/src/business/project-unlock.ts +260 -0
  50. package/src/business/project.ts +299 -0
  51. package/src/business/secret.test.ts +178 -0
  52. package/src/business/secret.ts +281 -0
  53. package/src/business/worker.test.ts +614 -0
  54. package/src/business/worker.ts +398 -0
  55. package/src/common/clock.ts +18 -0
  56. package/src/common/index.ts +5 -1
  57. package/src/common/performance.ts +44 -0
  58. package/src/common/random.ts +68 -0
  59. package/src/common/test/index.ts +2 -0
  60. package/src/common/test/render.ts +98 -0
  61. package/src/common/test/tracer.ts +359 -0
  62. package/src/common/tree.ts +33 -0
  63. package/src/common/utils.ts +40 -1
  64. package/src/config.ts +19 -11
  65. package/src/hotstate/abstractions.ts +48 -0
  66. package/src/hotstate/factory.ts +17 -0
  67. package/src/{secret → hotstate}/index.ts +1 -0
  68. package/src/hotstate/manager.ts +192 -0
  69. package/src/hotstate/memory.ts +100 -0
  70. package/src/hotstate/validation.ts +100 -0
  71. package/src/index.ts +2 -1
  72. package/src/library/abstractions.ts +24 -28
  73. package/src/library/factory.ts +2 -2
  74. package/src/library/local.ts +91 -111
  75. package/src/library/worker/evaluator.ts +36 -73
  76. package/src/library/worker/loader.lite.ts +54 -0
  77. package/src/library/worker/main.ts +15 -66
  78. package/src/library/worker/protocol.ts +6 -33
  79. package/src/lock/abstractions.ts +6 -0
  80. package/src/lock/factory.ts +15 -0
  81. package/src/lock/index.ts +4 -0
  82. package/src/lock/manager.ts +97 -0
  83. package/src/lock/memory.ts +19 -0
  84. package/src/lock/test.ts +108 -0
  85. package/src/orchestrator/manager.ts +118 -90
  86. package/src/orchestrator/operation-workset.ts +181 -93
  87. package/src/orchestrator/operation.ts +1021 -283
  88. package/src/project/abstractions.ts +27 -38
  89. package/src/project/evaluation.ts +248 -0
  90. package/src/project/factory.ts +1 -1
  91. package/src/project/index.ts +1 -2
  92. package/src/project/local.ts +107 -103
  93. package/src/pubsub/abstractions.ts +13 -0
  94. package/src/pubsub/factory.ts +19 -0
  95. package/src/{workspace → pubsub}/index.ts +1 -0
  96. package/src/pubsub/local.ts +36 -0
  97. package/src/pubsub/manager.ts +108 -0
  98. package/src/pubsub/validation.ts +33 -0
  99. package/src/runner/abstractions.ts +155 -68
  100. package/src/runner/artifact-env.ts +160 -0
  101. package/src/runner/factory.ts +20 -5
  102. package/src/runner/force-abort.ts +117 -0
  103. package/src/runner/local.ts +292 -372
  104. package/src/{common → runner}/pulumi.ts +89 -37
  105. package/src/services.ts +251 -40
  106. package/src/shared/index.ts +3 -11
  107. package/src/shared/models/backend/index.ts +3 -0
  108. package/src/shared/{library.ts → models/backend/library.ts} +4 -4
  109. package/src/shared/models/backend/project.ts +82 -0
  110. package/src/shared/models/backend/unlock-method.ts +20 -0
  111. package/src/shared/models/base.ts +68 -0
  112. package/src/shared/models/errors.ts +5 -0
  113. package/src/shared/models/index.ts +4 -0
  114. package/src/shared/models/project/api-key.ts +65 -0
  115. package/src/shared/models/project/artifact.ts +83 -0
  116. package/src/shared/models/project/index.ts +13 -0
  117. package/src/shared/models/project/lock.ts +91 -0
  118. package/src/shared/models/project/model.ts +14 -0
  119. package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
  120. package/src/shared/models/project/page.ts +57 -0
  121. package/src/shared/models/project/secret.ts +98 -0
  122. package/src/shared/models/project/service-account.ts +22 -0
  123. package/src/shared/models/project/state.ts +449 -0
  124. package/src/shared/models/project/terminal.ts +98 -0
  125. package/src/shared/models/project/trigger.ts +56 -0
  126. package/src/shared/models/project/unlock-method.ts +38 -0
  127. package/src/shared/models/project/worker.ts +107 -0
  128. package/src/shared/resolvers/graph-resolver.ts +61 -18
  129. package/src/shared/resolvers/index.ts +5 -0
  130. package/src/shared/resolvers/input-hash.ts +53 -15
  131. package/src/shared/resolvers/input.ts +47 -13
  132. package/src/shared/resolvers/registry.ts +3 -2
  133. package/src/shared/resolvers/state.ts +2 -2
  134. package/src/shared/resolvers/validation.ts +82 -25
  135. package/src/shared/utils/args.ts +25 -0
  136. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  137. package/src/shared/utils/hash.ts +6 -0
  138. package/src/shared/utils/index.ts +4 -0
  139. package/src/shared/utils/promise-tracker.ts +23 -0
  140. package/src/state/abstractions.ts +199 -131
  141. package/src/state/encryption.ts +98 -0
  142. package/src/state/factory.ts +3 -5
  143. package/src/state/index.ts +4 -0
  144. package/src/state/keyring.ts +22 -0
  145. package/src/state/local/backend.ts +106 -0
  146. package/src/state/local/collection.ts +361 -0
  147. package/src/state/local/index.ts +2 -0
  148. package/src/state/manager.ts +875 -18
  149. package/src/state/memory/backend.ts +70 -0
  150. package/src/state/memory/collection.ts +270 -0
  151. package/src/state/memory/index.ts +2 -0
  152. package/src/state/repository/index.ts +2 -0
  153. package/src/state/repository/repository.index.ts +193 -0
  154. package/src/state/repository/repository.ts +507 -0
  155. package/src/state/test.ts +457 -0
  156. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  157. package/src/terminal/docker.ts +18 -14
  158. package/src/terminal/factory.ts +3 -3
  159. package/src/terminal/index.ts +1 -1
  160. package/src/terminal/manager.ts +131 -79
  161. package/src/terminal/run.sh.ts +21 -11
  162. package/src/unlock/abstractions.ts +49 -0
  163. package/src/unlock/index.ts +2 -0
  164. package/src/unlock/memory.ts +32 -0
  165. package/src/worker/abstractions.ts +42 -0
  166. package/src/worker/docker.ts +83 -0
  167. package/src/worker/factory.ts +20 -0
  168. package/src/worker/index.ts +3 -0
  169. package/src/worker/manager.ts +167 -0
  170. package/dist/chunk-KTGKNSKM.js +0 -979
  171. package/dist/chunk-KTGKNSKM.js.map +0 -1
  172. package/dist/chunk-WXDYCRTT.js +0 -234
  173. package/dist/chunk-WXDYCRTT.js.map +0 -1
  174. package/src/library/worker/loader.ts +0 -114
  175. package/src/preferences/shared.ts +0 -1
  176. package/src/project/lock.ts +0 -39
  177. package/src/project/manager.ts +0 -433
  178. package/src/secret/abstractions.ts +0 -59
  179. package/src/secret/factory.ts +0 -22
  180. package/src/secret/local.ts +0 -152
  181. package/src/shared/project.ts +0 -62
  182. package/src/shared/state.ts +0 -247
  183. package/src/shared/terminal.ts +0 -14
  184. package/src/state/local.ts +0 -612
  185. package/src/workspace/abstractions.ts +0 -41
  186. package/src/workspace/factory.ts +0 -14
  187. package/src/workspace/local.ts +0 -54
@@ -1,7 +1,9 @@
1
1
  import type { LibraryBackend } from "../library"
2
2
  import type { ProjectBackend } from "../project"
3
- import type { StateBackend, StateManager } from "../state"
3
+ import type { StateManager } from "../state"
4
4
  import type { Logger } from "pino"
5
+ import type { InstanceLockService, InstanceStateService } from "../business"
6
+ import { EventEmitter, on } from "node:events"
5
7
  import {
6
8
  isUnitModel,
7
9
  parseInstanceId,
@@ -11,24 +13,26 @@ import {
11
13
  import { unique } from "remeda"
12
14
  import { BetterLock } from "better-lock"
13
15
  import {
14
- applyPartialInstanceState,
15
- createInstanceState,
16
- createInstanceStatePatch,
17
16
  InputHashResolver,
18
17
  InputResolver,
19
18
  type InputHashNode,
19
+ type InputHashOutput,
20
20
  type InputResolverNode,
21
+ type InstanceOperataionStatusEnum,
21
22
  type InstanceState,
22
- type InstanceStateUpdate,
23
- type InstanceStatus,
23
+ type InstanceStatePatch,
24
24
  type LibraryModel,
25
- type ProjectOperation,
25
+ type Operation,
26
+ type Project,
26
27
  type ResolvedInstanceInput,
27
28
  } from "../shared"
28
29
 
29
30
  export type OperationPhase = "update" | "destroy" | "refresh"
30
31
 
31
32
  export class OperationWorkset {
33
+ private readonly lockedInstanceIds = new Set<string>()
34
+ public readonly instanceIdsToLockIds = new Set<string>()
35
+
32
36
  private readonly instanceIdsToUpdate = new Set<string>()
33
37
  private readonly instanceIdsToDestroy = new Set<string>()
34
38
 
@@ -40,7 +44,7 @@ export class OperationWorkset {
40
44
  private readonly dependentStateIdMap = new Map<string, Set<string>>()
41
45
  private readonly stateChildIdMap = new Map<string, string[]>()
42
46
 
43
- private readonly unitSourceHashMap = new Map<string, string>()
47
+ private readonly unitSourceHashMap = new Map<string, number>()
44
48
 
45
49
  private inputResolver!: InputResolver
46
50
  private readonly inputResolverNodes = new Map<string, InputResolverNode>()
@@ -54,13 +58,29 @@ export class OperationWorkset {
54
58
  Record<string, ResolvedInstanceInput[]>
55
59
  >()
56
60
 
61
+ public currentPhase!: OperationPhase
62
+
63
+ private readonly lockEventEmitter = new EventEmitter()
64
+
57
65
  private constructor(
58
- public readonly operation: ProjectOperation,
66
+ public readonly project: Project,
67
+ public readonly operation: Operation,
59
68
  public readonly library: LibraryModel,
60
- private readonly stateManager: StateManager,
69
+ private readonly instanceLockService: InstanceLockService,
70
+ private readonly instanceStateService: InstanceStateService,
61
71
  private readonly logger: Logger,
62
72
  ) {}
63
73
 
74
+ public async ensureInstanceLocked(instanceId: string, signal: AbortSignal): Promise<void> {
75
+ if (this.lockedInstanceIds.has(instanceId)) {
76
+ return
77
+ }
78
+
79
+ for await (const _ of on(this.lockEventEmitter, instanceId, { signal })) {
80
+ return
81
+ }
82
+ }
83
+
64
84
  public getInstance(instanceId: string): InstanceModel {
65
85
  const instance = this.instanceMap.get(instanceId)
66
86
  if (!instance) {
@@ -70,35 +90,43 @@ export class OperationWorkset {
70
90
  return instance
71
91
  }
72
92
 
73
- public getAffectedInstanceIds(phase: OperationPhase): string[] {
74
- if (phase === "destroy") {
93
+ public getAffectedInstanceIds(): string[] {
94
+ if (this.currentPhase === "destroy") {
75
95
  return Array.from(this.instanceIdsToDestroy)
76
96
  }
77
97
 
78
98
  return Array.from(this.instanceIdsToUpdate)
79
99
  }
80
100
 
101
+ public getAllAffectedInstanceIds(): string[] {
102
+ return Array.from(this.instanceIdsToUpdate.union(this.instanceIdsToDestroy))
103
+ }
104
+
81
105
  public getInstanceOrUndefined(instanceId: string): InstanceModel | undefined {
82
106
  return this.instanceMap.get(instanceId)
83
107
  }
84
108
 
85
- public isAffected(instanceId: string, phase: OperationPhase): boolean {
86
- if (phase === "destroy") {
109
+ public isAffected(instanceId: string): boolean {
110
+ if (this.currentPhase === "destroy") {
87
111
  return this.instanceIdsToDestroy.has(instanceId)
88
112
  }
89
113
 
90
114
  return this.instanceIdsToUpdate.has(instanceId)
91
115
  }
92
116
 
93
- public updateState(update: InstanceStateUpdate, phase: OperationPhase): InstanceState {
94
- const finalState = applyPartialInstanceState(this.stateMap, update)
95
- this.stateManager.emitStatePatch(this.operation.projectId, createInstanceStatePatch(update))
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)
96
124
 
97
- if (finalState.parentId) {
98
- this.recalculateCompositeInstanceState(finalState.parentId, phase)
125
+ if (state.parentId) {
126
+ await this.recalculateCompositeInstanceState(state.parentId)
99
127
  }
100
128
 
101
- return finalState
129
+ return state
102
130
  }
103
131
 
104
132
  public setState(state: InstanceState): void {
@@ -115,7 +143,7 @@ export class OperationWorkset {
115
143
  children.push(state.id)
116
144
  }
117
145
 
118
- for (const dependencyId of state.dependencyIds) {
146
+ for (const dependencyId of state.operationStatus?.dependencyIds ?? []) {
119
147
  this.addDependentState(state.id, dependencyId)
120
148
  }
121
149
  }
@@ -131,35 +159,52 @@ export class OperationWorkset {
131
159
  dependentStates.add(instanceId)
132
160
  }
133
161
 
134
- public restoreInitialStatus(instanceId: string): void {
135
- const state = this.stateMap.get(instanceId)
136
- const initialState = this.initialStateMap.get(instanceId)
162
+ public async restoreInitialState(instanceId: string): Promise<void> {
163
+ const initialState = this.initialStateMap.get(instanceId) ?? { id: instanceId }
137
164
 
138
- if (!state || !initialState) {
139
- this.logger.warn(
140
- `cannot reset status for instance "${instanceId}" because it is not in the state map`,
141
- )
142
- return
143
- }
165
+ this.logger.debug(
166
+ { initialState },
167
+ `resetting state for instance "%s" to initial state`,
168
+ instanceId,
169
+ )
144
170
 
145
- this.stateMap.set(instanceId, { ...initialState, status: initialState.status })
171
+ await this.instanceStateService.updateInstanceStates(
172
+ this.project.id,
173
+ [initialState],
174
+ true,
175
+ false,
176
+ )
177
+
178
+ if (initialState.parentId) {
179
+ await this.recalculateCompositeInstanceState(initialState.parentId)
180
+ }
146
181
  }
147
182
 
148
- public emitAffectedInitialStates(): void {
149
- for (const state of this.initialStateMap.values()) {
150
- if (this.instanceIdsToUpdate.has(state.id)) {
151
- this.stateManager.emitStatePatch(this.operation.projectId, state)
183
+ public async restoreAffectedInitialStates(): Promise<void> {
184
+ const instanceStates: InstanceState[] = []
185
+
186
+ for (const instanceId of this.getAffectedInstanceIds()) {
187
+ const state = this.initialStateMap.get(instanceId)
188
+ if (state) {
189
+ instanceStates.push(state)
152
190
  }
153
191
  }
192
+
193
+ await this.instanceStateService.updateInstanceStates(
194
+ this.project.id,
195
+ instanceStates,
196
+ false,
197
+ false,
198
+ )
154
199
  }
155
200
 
156
- public getAffectedCompositeChildren(instanceId: string, phase: OperationPhase): InstanceModel[] {
201
+ public getAffectedCompositeChildren(instanceId: string): InstanceModel[] {
157
202
  const children = this.instanceChildrenMap.get(instanceId)
158
203
  if (!children) {
159
204
  return []
160
205
  }
161
206
 
162
- if (phase === "destroy") {
207
+ if (this.currentPhase === "destroy") {
163
208
  return children.filter(child => this.instanceIdsToDestroy.has(child.id))
164
209
  }
165
210
 
@@ -170,11 +215,11 @@ export class OperationWorkset {
170
215
  return this.stateMap.get(instanceId)
171
216
  }
172
217
 
173
- public getParentId(instanceId: string, phase: OperationPhase): string | null {
174
- if (phase === "destroy") {
218
+ public getParentId(instanceId: string): string | undefined {
219
+ if (this.currentPhase === "destroy") {
175
220
  const state = this.stateMap.get(instanceId)
176
221
  if (!state) {
177
- return null
222
+ return undefined
178
223
  }
179
224
 
180
225
  return state.parentId
@@ -182,7 +227,7 @@ export class OperationWorkset {
182
227
 
183
228
  const instance = this.getInstance(instanceId)
184
229
 
185
- return instance.parentId ?? null
230
+ return instance.parentId
186
231
  }
187
232
 
188
233
  public getDependentStates(instanceId: string): InstanceState[] {
@@ -196,8 +241,15 @@ export class OperationWorkset {
196
241
  .filter((state): state is InstanceState => !!state)
197
242
  }
198
243
 
199
- private recalculateCompositeInstanceState(instanceId: string, phase: OperationPhase): void {
200
- const state = this.stateMap.get(instanceId) ?? createInstanceState(instanceId)
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
+ }
252
+
201
253
  let currentResourceCount = 0
202
254
  let totalResourceCount = 0
203
255
 
@@ -205,28 +257,25 @@ export class OperationWorkset {
205
257
  for (const childId of children) {
206
258
  const child = this.stateMap.get(childId)
207
259
 
208
- if (child?.currentResourceCount) {
209
- currentResourceCount += child.currentResourceCount
260
+ if (child?.operationStatus?.currentResourceCount) {
261
+ currentResourceCount += child.operationStatus.currentResourceCount
210
262
  }
211
263
 
212
- if (child?.totalResourceCount) {
213
- totalResourceCount += child.totalResourceCount
264
+ if (child?.operationStatus?.totalResourceCount) {
265
+ totalResourceCount += child.operationStatus?.totalResourceCount
214
266
  }
215
267
  }
216
268
 
217
- const updatedState = {
218
- ...state,
219
- status: OperationWorkset.getStatusByOperationPhase(phase),
220
- currentResourceCount,
221
- totalResourceCount,
269
+ const patch: InstanceStatePatch = {
270
+ id: instanceId,
271
+ operationStatus: {
272
+ status: this.getTransientStatusByOperationPhase(),
273
+ currentResourceCount,
274
+ totalResourceCount,
275
+ },
222
276
  }
223
277
 
224
- this.stateMap.set(instanceId, updatedState)
225
- this.stateManager.emitStatePatch(this.operation.projectId, updatedState)
226
-
227
- if (state.parentId) {
228
- this.recalculateCompositeInstanceState(state.parentId, phase)
229
- }
278
+ await this.patchState(patch)
230
279
  }
231
280
 
232
281
  private addInstance(instance: InstanceModel): void {
@@ -281,7 +330,11 @@ export class OperationWorkset {
281
330
  return
282
331
  }
283
332
 
284
- if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
333
+ if (
334
+ !state?.operationStatus ||
335
+ state.operationStatus.status === "error" ||
336
+ state.operationStatus.inputHash !== expectedInputHash
337
+ ) {
285
338
  this.instanceIdsToUpdate.add(instanceId)
286
339
  }
287
340
  }
@@ -312,7 +365,11 @@ export class OperationWorkset {
312
365
  const state = this.stateMap.get(child.id)
313
366
  const { inputHash: expectedInputHash } = this.inputHashResolver.requireOutput(child.id)
314
367
 
315
- if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
368
+ if (
369
+ !state?.operationStatus ||
370
+ state.operationStatus.status === "error" ||
371
+ state.operationStatus.inputHash !== expectedInputHash
372
+ ) {
316
373
  this.instanceIdsToUpdate.add(child.id)
317
374
  }
318
375
  }
@@ -347,7 +404,7 @@ export class OperationWorkset {
347
404
  }
348
405
 
349
406
  const state = this.stateMap.get(instanceKey)
350
- if (!state || state.status === "not_created") {
407
+ if (!state) {
351
408
  return
352
409
  }
353
410
 
@@ -438,7 +495,7 @@ export class OperationWorkset {
438
495
  private getSourceHashIfApplicable(
439
496
  instance: InstanceModel,
440
497
  component: ComponentModel,
441
- ): string | undefined {
498
+ ): number | undefined {
442
499
  if (isUnitModel(component)) {
443
500
  return this.unitSourceHashMap.get(instance.type)
444
501
  }
@@ -446,8 +503,8 @@ export class OperationWorkset {
446
503
  return undefined
447
504
  }
448
505
 
449
- private static getStatusByOperationPhase(phase: OperationPhase): InstanceStatus {
450
- switch (phase) {
506
+ private getTransientStatusByOperationPhase(): InstanceOperataionStatusEnum {
507
+ switch (this.currentPhase) {
451
508
  case "update":
452
509
  return "updating"
453
510
  case "destroy":
@@ -457,7 +514,7 @@ export class OperationWorkset {
457
514
  }
458
515
  }
459
516
 
460
- public async getUpToDateInputHash(instance: InstanceModel): Promise<string> {
517
+ public async getUpToDateInputHashOutput(instance: InstanceModel): Promise<InputHashOutput> {
461
518
  return await this.inputHashResolverLock.acquire(async () => {
462
519
  const component = this.library.components[instance.type]
463
520
 
@@ -472,67 +529,90 @@ export class OperationWorkset {
472
529
  this.inputHashResolver.invalidate(instance.id)
473
530
  await this.inputHashResolver.process()
474
531
 
475
- const { inputHash } = this.inputHashResolver.requireOutput(instance.id)
476
- return inputHash
532
+ return this.inputHashResolver.requireOutput(instance.id)
477
533
  })
478
534
  }
479
535
 
480
- public getLockInstanceIds(): string[] {
481
- return Array.from(this.instanceIdsToUpdate.union(this.instanceIdsToDestroy))
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)
561
+ }
482
562
  }
483
563
 
484
564
  public static async load(
485
- operation: ProjectOperation,
565
+ project: Project,
566
+ operation: Operation,
486
567
  projectBackend: ProjectBackend,
487
568
  libraryBackend: LibraryBackend,
488
- stateBackend: StateBackend,
489
569
  stateManager: StateManager,
570
+ instanceLockService: InstanceLockService,
571
+ instanceStateService: InstanceStateService,
490
572
  logger: Logger,
491
573
  signal: AbortSignal,
492
574
  ): Promise<OperationWorkset> {
493
- const [library, project, compositeInstances, states] = await Promise.all([
494
- libraryBackend.loadLibrary(signal),
495
- projectBackend.getProject(operation.projectId, signal),
496
- stateBackend.getCompositeInstances(operation.projectId, signal),
497
- stateBackend.getAllInstanceStates(operation.projectId, signal),
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),
498
581
  ])
499
582
 
500
583
  const workset = new OperationWorkset(
584
+ project,
501
585
  operation,
502
586
  library,
503
- stateManager,
587
+ instanceLockService,
588
+ instanceStateService,
504
589
  logger.child({ operationId: operation.id, service: "OperationWorkset" }),
505
590
  )
506
591
 
507
592
  // prepare instances
508
- for (const instance of project.instances) {
593
+ for (const instance of instances) {
509
594
  workset.addInstance(instance)
510
595
  }
511
596
 
512
- for (const instance of compositeInstances) {
513
- const worksetInstance = workset.instanceMap.get(instance.instance.id)
597
+ for (const instance of virtualInstances) {
598
+ const worksetInstance = workset.instanceMap.get(instance.id)
514
599
 
515
600
  if (worksetInstance) {
516
- worksetInstance.outputs = instance.instance.outputs
517
- worksetInstance.resolvedOutputs = instance.instance.resolvedOutputs
518
- } else if (instance.instance.parentId) {
519
- workset.addInstance(instance.instance)
601
+ worksetInstance.outputs = instance.outputs
602
+ worksetInstance.resolvedOutputs = instance.resolvedOutputs
603
+ } else if (instance.parentId) {
604
+ workset.addInstance(instance)
520
605
  } else {
521
606
  workset.logger.warn(
522
- `ignoring instance "${instance.instance.id}" from composite instance state because it is not in the project or is not a part of known composite instance`,
607
+ `ignoring virtual instance "${instance.id}" because it is not in the project or is not a part of known composite instance`,
523
608
  )
524
609
  continue
525
610
  }
526
-
527
- for (const child of instance.children) {
528
- if (!workset.instanceMap.has(child.id)) {
529
- workset.addInstance(child)
530
- }
531
- }
532
611
  }
533
612
 
534
613
  const unitSources = await libraryBackend.getResolvedUnitSources(
535
- unique(Array.from(workset.instanceMap.values().map(i => i.type))),
614
+ project.libraryId,
615
+ unique(Array.from(workset.instanceMap.values()).map(i => i.type)),
536
616
  )
537
617
 
538
618
  for (const unitSource of unitSources) {
@@ -559,7 +639,7 @@ export class OperationWorkset {
559
639
  })
560
640
  }
561
641
 
562
- for (const hub of project.hubs) {
642
+ for (const hub of hubs) {
563
643
  workset.inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub })
564
644
  }
565
645
 
@@ -600,6 +680,14 @@ export class OperationWorkset {
600
680
 
601
681
  workset.calculateInstanceIdsToDestroy()
602
682
 
683
+ for (const instanceId of workset.instanceIdsToUpdate) {
684
+ workset.instanceIdsToLockIds.add(instanceId)
685
+ }
686
+
687
+ for (const instanceId of workset.instanceIdsToDestroy) {
688
+ workset.instanceIdsToLockIds.add(instanceId)
689
+ }
690
+
603
691
  return workset
604
692
  }
605
693
  }