@highstate/backend 0.7.1 → 0.7.3

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 (74) hide show
  1. package/dist/{index.mjs → index.js} +1255 -916
  2. package/dist/library/source-resolution-worker.js +55 -0
  3. package/dist/library/worker/main.js +207 -0
  4. package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
  5. package/dist/shared/index.js +6 -0
  6. package/dist/utils-ByadNcv4.js +102 -0
  7. package/package.json +14 -18
  8. package/src/common/index.ts +3 -0
  9. package/src/common/local.ts +22 -0
  10. package/src/common/pulumi.ts +230 -0
  11. package/src/common/utils.ts +137 -0
  12. package/src/config.ts +40 -0
  13. package/src/index.ts +6 -0
  14. package/src/library/abstractions.ts +83 -0
  15. package/src/library/factory.ts +20 -0
  16. package/src/library/index.ts +2 -0
  17. package/src/library/local.ts +404 -0
  18. package/src/library/source-resolution-worker.ts +96 -0
  19. package/src/library/worker/evaluator.ts +119 -0
  20. package/src/library/worker/loader.ts +93 -0
  21. package/src/library/worker/main.ts +82 -0
  22. package/src/library/worker/protocol.ts +38 -0
  23. package/src/orchestrator/index.ts +1 -0
  24. package/src/orchestrator/manager.ts +165 -0
  25. package/src/orchestrator/operation-workset.ts +483 -0
  26. package/src/orchestrator/operation.ts +647 -0
  27. package/src/preferences/shared.ts +1 -0
  28. package/src/project/abstractions.ts +89 -0
  29. package/src/project/factory.ts +11 -0
  30. package/src/project/index.ts +4 -0
  31. package/src/project/local.ts +412 -0
  32. package/src/project/lock.ts +39 -0
  33. package/src/project/manager.ts +374 -0
  34. package/src/runner/abstractions.ts +146 -0
  35. package/src/runner/factory.ts +22 -0
  36. package/src/runner/index.ts +2 -0
  37. package/src/runner/local.ts +698 -0
  38. package/src/secret/abstractions.ts +59 -0
  39. package/src/secret/factory.ts +22 -0
  40. package/src/secret/index.ts +2 -0
  41. package/src/secret/local.ts +152 -0
  42. package/src/services.ts +133 -0
  43. package/src/shared/index.ts +10 -0
  44. package/src/shared/library.ts +77 -0
  45. package/src/shared/operation.ts +85 -0
  46. package/src/shared/project.ts +62 -0
  47. package/src/shared/resolvers/graph-resolver.ts +111 -0
  48. package/src/shared/resolvers/input-hash.ts +77 -0
  49. package/src/shared/resolvers/input.ts +314 -0
  50. package/src/shared/resolvers/registry.ts +10 -0
  51. package/src/shared/resolvers/validation.ts +94 -0
  52. package/src/shared/state.ts +262 -0
  53. package/src/shared/terminal.ts +13 -0
  54. package/src/state/abstractions.ts +222 -0
  55. package/src/state/factory.ts +22 -0
  56. package/src/state/index.ts +3 -0
  57. package/src/state/local.ts +605 -0
  58. package/src/state/manager.ts +33 -0
  59. package/src/terminal/docker.ts +90 -0
  60. package/src/terminal/factory.ts +20 -0
  61. package/src/terminal/index.ts +3 -0
  62. package/src/terminal/manager.ts +330 -0
  63. package/src/terminal/run.sh.ts +37 -0
  64. package/src/terminal/shared.ts +50 -0
  65. package/src/workspace/abstractions.ts +41 -0
  66. package/src/workspace/factory.ts +14 -0
  67. package/src/workspace/index.ts +2 -0
  68. package/src/workspace/local.ts +54 -0
  69. package/dist/index.d.ts +0 -760
  70. package/dist/library/worker/main.mjs +0 -164
  71. package/dist/runner/source-resolution-worker.mjs +0 -22
  72. package/dist/shared/index.d.ts +0 -85
  73. package/dist/shared/index.mjs +0 -54
  74. package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -0,0 +1,647 @@
1
+ import type { LibraryBackend } from "../library"
2
+ import type { LogEntry, StateBackend, StateManager } from "../state"
3
+ import type { ProjectBackend, ProjectLock } from "../project"
4
+ import type { SecretBackend } from "../secret"
5
+ import type { RunnerBackend } from "../runner"
6
+ import type { InstanceLogsEvents, OperationEvents } from "./manager"
7
+ import type { EventEmitter } from "node:events"
8
+ import type { Logger } from "pino"
9
+ import { isUnitModel, parseInstanceId, type InstanceModel } from "@highstate/contract"
10
+ import { mapValues } from "remeda"
11
+ import {
12
+ type InstanceState,
13
+ type InstanceStateUpdate,
14
+ type InstanceStatus,
15
+ type ProjectOperation,
16
+ type InstanceTriggerInvocation,
17
+ createInstanceState,
18
+ } from "../shared"
19
+ import {
20
+ createAsyncBatcher,
21
+ errorToString,
22
+ isAbortError,
23
+ isAbortErrorLike,
24
+ tryWrapAbortErrorLike,
25
+ valueToString,
26
+ } from "../common"
27
+ import { OperationWorkset } from "./operation-workset"
28
+
29
+ type OperationPhase = "update" | "destroy" | "refresh"
30
+
31
+ export class RuntimeOperation {
32
+ private readonly abortController = new AbortController()
33
+ private readonly instancePromiseMap = new Map<string, Promise<void>>()
34
+ private workset!: OperationWorkset
35
+
36
+ private currentPhase!: OperationPhase
37
+
38
+ constructor(
39
+ private readonly operation: ProjectOperation,
40
+ private readonly runnerBackend: RunnerBackend,
41
+ private readonly stateBackend: StateBackend,
42
+ private readonly libraryBackend: LibraryBackend,
43
+ private readonly projectBackend: ProjectBackend,
44
+ private readonly secretBackend: SecretBackend,
45
+ private readonly projectLock: ProjectLock,
46
+ private readonly stateManager: StateManager,
47
+ private readonly operationEE: EventEmitter<OperationEvents>,
48
+ private readonly instanceLogsEE: EventEmitter<InstanceLogsEvents>,
49
+ private readonly logger: Logger,
50
+ ) {}
51
+
52
+ async operateSafe(): Promise<void> {
53
+ try {
54
+ await this.operate()
55
+ } catch (error) {
56
+ if (isAbortError(error)) {
57
+ this.logger.info("the operation was cancelled")
58
+ this.operation.status = "cancelled"
59
+
60
+ await this.updateOperation()
61
+ return
62
+ }
63
+
64
+ this.logger.error({ msg: "an error occurred while running the operation", error })
65
+
66
+ this.operation.status = "failed"
67
+ this.operation.error = errorToString(error)
68
+
69
+ await this.updateOperation()
70
+ } finally {
71
+ await Promise.all([
72
+ this.persistStates.flush(),
73
+ this.persistLogs.flush(),
74
+ this.persistSecrets.flush(),
75
+ ])
76
+
77
+ this.logger.debug("operation finished, all entries persisted")
78
+ }
79
+ }
80
+
81
+ private async operate(): Promise<void> {
82
+ this.logger.info("starting operation")
83
+ let lockInstanceIds: string[]
84
+
85
+ // keep recalculating the workset until we can acquire the locks and actually start the operation
86
+ while (true) {
87
+ this.workset = await OperationWorkset.load(
88
+ this.operation,
89
+ this.projectBackend,
90
+ this.libraryBackend,
91
+ this.stateBackend,
92
+ this.stateManager,
93
+ this.logger,
94
+ this.abortController.signal,
95
+ )
96
+
97
+ lockInstanceIds = this.workset.getLockInstanceIds()
98
+
99
+ if (this.projectLock.canImmediatelyAcquireLocks(lockInstanceIds)) {
100
+ break
101
+ }
102
+
103
+ this.logger.info("waiting for locks to be available")
104
+
105
+ await this.projectLock.lockInstances(lockInstanceIds, () => Promise.resolve())
106
+ }
107
+
108
+ try {
109
+ // actually acquire the locks and start the operation
110
+ await this.projectLock.lockInstances(lockInstanceIds, () => this.processOperation())
111
+ } finally {
112
+ if (this.operation.type === "preview") {
113
+ // stream initial states for preview operations
114
+ this.workset.emitAffectedInitialStates()
115
+ }
116
+ }
117
+ }
118
+
119
+ private async processOperation(): Promise<void> {
120
+ this.operation.affectedInstanceIds = this.workset.operation.affectedInstanceIds
121
+
122
+ this.logger.info(
123
+ { affectedInstanceIds: this.operation.affectedInstanceIds },
124
+ "operation started",
125
+ )
126
+
127
+ const phases = this.getOperationPhases()
128
+
129
+ for (const phase of phases) {
130
+ this.currentPhase = phase
131
+
132
+ const promises: Promise<void>[] = []
133
+ for (const instanceId of this.operation.affectedInstanceIds) {
134
+ const instance = this.workset.getInstance(instanceId)
135
+ if (instance.parentId && this.workset.isAffected(instance.parentId)) {
136
+ // do not call the operation for child instances of affected composites,
137
+ // they will be called by their parent instance
138
+ continue
139
+ }
140
+
141
+ promises.push(this.getInstancePromiseForOperation(instanceId))
142
+ }
143
+
144
+ this.logger.info(`all operations for phase "%s" started`, phase)
145
+ this.operation.status = "running"
146
+ await this.updateOperation()
147
+
148
+ await Promise.all(promises)
149
+
150
+ this.logger.info(`all operations for phase "%s" completed`, phase)
151
+ }
152
+
153
+ this.operation.status = "completed"
154
+ this.operation.error = null
155
+ await this.updateOperation()
156
+
157
+ this.logger.info("operation completed")
158
+ }
159
+
160
+ cancel() {
161
+ this.logger.info("cancelling operation")
162
+ this.abortController.abort()
163
+ }
164
+
165
+ private getInstancePromiseForOperation(instanceId: string): Promise<void> {
166
+ const instance = this.workset.getInstance(instanceId)
167
+ const component = this.workset.library.components[instance.type]
168
+
169
+ if (isUnitModel(component)) {
170
+ return this.getUnitPromise(instance)
171
+ }
172
+
173
+ return this.getCompositePromise(instance)
174
+ }
175
+
176
+ private getOperationPhases(): OperationPhase[] {
177
+ switch (this.operation.type) {
178
+ case "update":
179
+ case "preview":
180
+ return ["update"]
181
+ case "recreate":
182
+ return ["destroy", "update"]
183
+ case "destroy":
184
+ return ["destroy"]
185
+ case "refresh":
186
+ return ["refresh"]
187
+ }
188
+ }
189
+
190
+ private async getUnitPromise(instance: InstanceModel): Promise<void> {
191
+ switch (this.currentPhase) {
192
+ case "update": {
193
+ return this.updateUnit(instance)
194
+ }
195
+ case "destroy": {
196
+ return this.destroyUnit(instance.id)
197
+ }
198
+ case "refresh": {
199
+ return this.refreshUnit(instance.id)
200
+ }
201
+ }
202
+ }
203
+
204
+ private async getCompositePromise(instance: InstanceModel): Promise<void> {
205
+ const logger = this.logger.child({ instanceId: instance.id })
206
+
207
+ return this.getInstancePromise(instance.id, async () => {
208
+ const state = this.workset.getState(instance.id) ?? createInstanceState(instance.id)
209
+
210
+ this.updateInstanceState({
211
+ ...state,
212
+ parentId: instance.parentId,
213
+ latestOperationId: this.operation.id,
214
+ status: this.getStatusByOperationType(),
215
+ error: null,
216
+ })
217
+
218
+ const children = this.workset.getAffectedCompositeChildren(instance.id)
219
+ const childPromises: Promise<void>[] = []
220
+
221
+ if (children.length) {
222
+ logger.info("running %s children", children.length)
223
+ } else {
224
+ logger.warn("no affected children found for composite component")
225
+ }
226
+
227
+ for (const child of children) {
228
+ logger.debug(`waiting for child: "%s"`, child.id)
229
+ childPromises.push(this.getInstancePromiseForOperation(child.id))
230
+ }
231
+
232
+ try {
233
+ await Promise.all(childPromises)
234
+
235
+ if (children.length > 0) {
236
+ logger.info("all children completed")
237
+ }
238
+
239
+ this.updateInstanceState({
240
+ id: instance.id,
241
+ status: this.operation.type === "destroy" ? "not_created" : "created",
242
+ inputHash: await this.workset.getUpToDateInputHash(instance),
243
+ })
244
+ } catch (error) {
245
+ if (isAbortErrorLike(error)) {
246
+ this.workset.restoreInitialStatus(instance.id)
247
+ return
248
+ }
249
+
250
+ this.updateInstanceState({
251
+ id: instance.id,
252
+ status: "error",
253
+ error: errorToString(error),
254
+ })
255
+ }
256
+ })
257
+ }
258
+
259
+ private updateUnit(instance: InstanceModel): Promise<void> {
260
+ return this.getInstancePromise(instance.id, async logger => {
261
+ this.updateInstanceState({
262
+ id: instance.id,
263
+ parentId: instance.parentId,
264
+ latestOperationId: this.operation.id,
265
+ status: "pending",
266
+ error: null,
267
+ currentResourceCount: 0,
268
+ })
269
+
270
+ let dependencyIds: string[] = []
271
+ try {
272
+ dependencyIds = await this.updateUnitDependencies(instance, logger)
273
+ } catch (error) {
274
+ // restore the initial status of the instance if one of the dependencies failed
275
+ this.workset.restoreInitialStatus(instance.id)
276
+ throw error
277
+ }
278
+
279
+ logger.info("updating unit")
280
+
281
+ const secrets = await this.secretBackend.get(this.operation.projectId, instance.id)
282
+ this.abortController.signal.throwIfAborted()
283
+
284
+ logger.debug("secrets loaded", { count: Object.keys(secrets).length })
285
+
286
+ await this.runnerBackend[this.operation.type === "preview" ? "preview" : "update"]({
287
+ projectId: this.operation.projectId,
288
+ instanceType: instance.type,
289
+ instanceName: instance.name,
290
+ config: this.prepareUnitConfig(instance),
291
+ refresh: this.operation.options.refresh,
292
+ secrets: mapValues(secrets, value => valueToString(value)),
293
+ signal: this.abortController.signal,
294
+ })
295
+
296
+ logger.debug("unit update requested")
297
+
298
+ const stream = this.runnerBackend.watch({
299
+ projectId: this.operation.projectId,
300
+ instanceType: instance.type,
301
+ instanceName: instance.name,
302
+ finalStatuses: ["created", "error"],
303
+ })
304
+
305
+ await this.watchStateStream(stream)
306
+
307
+ const inputHash = await this.workset.getUpToDateInputHash(instance)
308
+
309
+ this.updateInstanceState({
310
+ id: instance.id,
311
+ inputHash,
312
+ dependencyIds,
313
+ })
314
+
315
+ logger.debug("input hash after update", { inputHash })
316
+ logger.info("unit updated")
317
+ })
318
+ }
319
+
320
+ private async updateUnitDependencies(instance: InstanceModel, logger: Logger): Promise<string[]> {
321
+ try {
322
+ const dependencies = this.getInstanceDependencies(instance)
323
+ const dependencyPromises: Promise<void>[] = []
324
+
325
+ for (const dependency of dependencies) {
326
+ if (!this.operation.affectedInstanceIds.includes(dependency.id)) {
327
+ // skip dependencies that are not affected by the operation
328
+ continue
329
+ }
330
+
331
+ logger.debug(`waiting for dependency: ${dependency.id}`)
332
+ dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id))
333
+ }
334
+
335
+ await Promise.all(dependencyPromises)
336
+
337
+ if (dependencies.length > 0) {
338
+ logger.info("all dependencies completed")
339
+ }
340
+
341
+ return dependencies.map(dependency => dependency.id)
342
+ } catch (error) {
343
+ // restore the initial status of the instance if one of the dependencies failed
344
+ this.workset.restoreInitialStatus(instance.id)
345
+ throw error
346
+ }
347
+ }
348
+
349
+ private async processBeforeDestroyTriggers(state: InstanceState, logger: Logger): Promise<void> {
350
+ if (!this.operation.options.invokeDestroyTriggers) {
351
+ logger.debug("destroy triggers are disabled for the operation")
352
+ return
353
+ }
354
+
355
+ const instance = this.workset.getInstance(state.id)
356
+
357
+ const triggers = state.triggers.filter(trigger => trigger.spec.type === "before-destroy")
358
+ if (triggers.length === 0) {
359
+ return
360
+ }
361
+
362
+ const invokedTriggers = triggers.map(trigger => ({ name: trigger.name }))
363
+
364
+ logger.info("updating unit to process before-destroy triggers...")
365
+
366
+ const secrets = await this.secretBackend.get(
367
+ this.operation.projectId,
368
+ instance.id,
369
+ this.abortController.signal,
370
+ )
371
+
372
+ await this.runnerBackend.update({
373
+ projectId: this.operation.projectId,
374
+ instanceType: instance.type,
375
+ instanceName: instance.name,
376
+ config: this.prepareUnitConfig(instance, invokedTriggers),
377
+ refresh: this.operation.options.refresh,
378
+ secrets: mapValues(secrets, value => valueToString(value)),
379
+ signal: this.abortController.signal,
380
+ })
381
+
382
+ logger.debug("unit update requested")
383
+
384
+ const stream = this.runnerBackend.watch({
385
+ projectId: this.operation.projectId,
386
+ instanceType: instance.type,
387
+ instanceName: instance.name,
388
+ finalStatuses: ["created", "error"],
389
+ })
390
+
391
+ await this.watchStateStream(stream)
392
+
393
+ logger.debug("before-destroy triggers processed")
394
+ }
395
+
396
+ private async destroyUnit(instanceId: string): Promise<void> {
397
+ return this.getInstancePromise(instanceId, async logger => {
398
+ this.updateInstanceState({
399
+ id: instanceId,
400
+ latestOperationId: this.operation.id,
401
+ status: "pending",
402
+ error: null,
403
+ })
404
+
405
+ const state = this.workset.getState(instanceId)
406
+ if (!state) {
407
+ logger.warn("state not found for unit, but destroy was requested")
408
+ return
409
+ }
410
+
411
+ const dependentPromises: Promise<void>[] = []
412
+ const dependents = this.workset.getDependentStates(state.id)
413
+
414
+ for (const dependent of dependents) {
415
+ dependentPromises.push(this.getInstancePromiseForOperation(dependent.id))
416
+ }
417
+
418
+ await Promise.all(dependentPromises)
419
+ this.abortController.signal.throwIfAborted()
420
+
421
+ await this.processBeforeDestroyTriggers(state, logger)
422
+
423
+ logger.info("destroying unit...")
424
+
425
+ const [type, name] = parseInstanceId(instanceId)
426
+
427
+ await this.runnerBackend.destroy({
428
+ projectId: this.operation.projectId,
429
+ instanceType: type,
430
+ instanceName: name,
431
+ refresh: this.operation.options.refresh,
432
+ signal: this.abortController.signal,
433
+ deleteUnreachable: this.operation.options.deleteUnreachableResources,
434
+ })
435
+
436
+ this.logger.debug("destroy request sent")
437
+
438
+ const stream = this.runnerBackend.watch({
439
+ projectId: this.operation.projectId,
440
+ instanceType: type,
441
+ instanceName: name,
442
+ finalStatuses: ["not_created", "error"],
443
+ })
444
+
445
+ await this.watchStateStream(stream)
446
+ this.logger.info("unit destroyed")
447
+ })
448
+ }
449
+
450
+ private async refreshUnit(instanceId: string) {
451
+ const logger = this.logger.child({ instanceId })
452
+
453
+ return this.getInstancePromise(instanceId, async () => {
454
+ this.updateInstanceState({
455
+ id: instanceId,
456
+ latestOperationId: this.operation.id,
457
+ status: "refreshing",
458
+ currentResourceCount: 0,
459
+ totalResourceCount: 0,
460
+ })
461
+
462
+ logger.info("refreshing unit...")
463
+
464
+ const [type, name] = parseInstanceId(instanceId)
465
+
466
+ await this.runnerBackend.refresh({
467
+ projectId: this.operation.projectId,
468
+ instanceType: type,
469
+ instanceName: name,
470
+ signal: this.abortController.signal,
471
+ })
472
+
473
+ logger.debug("unit refresh requested")
474
+
475
+ const stream = this.runnerBackend.watch({
476
+ projectId: this.operation.projectId,
477
+ instanceType: type,
478
+ instanceName: name,
479
+ finalStatuses: ["created", "error"],
480
+ })
481
+
482
+ await this.watchStateStream(stream)
483
+ logger.info("unit refreshed")
484
+ })
485
+ }
486
+
487
+ private async watchStateStream(stream: AsyncIterable<InstanceStateUpdate>) {
488
+ let statePatch: InstanceStateUpdate | undefined
489
+ for await (statePatch of stream) {
490
+ if (statePatch.status === "not_created" && this.operation.type === "recreate") {
491
+ // do not stream "not_created" status for recreate operation to improve UX
492
+ continue
493
+ }
494
+
495
+ this.updateInstanceState(statePatch)
496
+ }
497
+
498
+ if (!statePatch) {
499
+ throw new Error("The stream ended without emitting any state.")
500
+ }
501
+
502
+ if (statePatch.status === "error") {
503
+ throw tryWrapAbortErrorLike(
504
+ new Error(`The operation on unit "${statePatch.id}" failed: ${statePatch.error}`),
505
+ )
506
+ }
507
+ }
508
+
509
+ private prepareUnitConfig(
510
+ instance: InstanceModel,
511
+ invokedTriggers: InstanceTriggerInvocation[] = [],
512
+ ): Record<string, string> {
513
+ const config: Record<string, string> = {}
514
+
515
+ for (const [key, value] of Object.entries(instance.args ?? {})) {
516
+ config[key] = valueToString(value)
517
+ }
518
+
519
+ const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
520
+
521
+ for (const [key, value] of Object.entries(instanceInputs)) {
522
+ config[`input.${key}`] = JSON.stringify(value.map(input => input.input))
523
+ }
524
+
525
+ config["$invokedTriggers"] = JSON.stringify(invokedTriggers)
526
+
527
+ return config
528
+ }
529
+
530
+ private async updateOperation(): Promise<void> {
531
+ this.operationEE.emit(this.operation.projectId, this.operation)
532
+ await this.stateBackend.putOperation(this.operation)
533
+ }
534
+
535
+ private updateInstanceState(patch: InstanceStateUpdate): void {
536
+ if (!patch.id) {
537
+ throw new Error("The ID of the instance state is required.")
538
+ }
539
+
540
+ if (patch.logLine) {
541
+ // recursively persist logs for instance and all parent instances
542
+
543
+ let instance: InstanceModel | null = this.workset.getInstance(patch.id)
544
+
545
+ for (;;) {
546
+ this.persistLogs.call([instance.id, patch.logLine])
547
+ this.instanceLogsEE.emit(`${this.operation.id}/${instance.id}`, patch.logLine)
548
+
549
+ if (!instance.parentId) {
550
+ break
551
+ }
552
+
553
+ instance = this.workset.getInstance(instance.parentId)
554
+ }
555
+ return
556
+ }
557
+
558
+ const state = this.workset.updateState(patch)
559
+
560
+ // do not persist anyting for preview operations
561
+ if (this.operation.type !== "preview") {
562
+ this.persistStates.call(state)
563
+
564
+ if (patch.secrets) {
565
+ this.persistSecrets.call([patch.id, patch.secrets])
566
+ }
567
+ }
568
+ }
569
+
570
+ private getInstancePromise(
571
+ instanceId: string,
572
+ fn: (logger: Logger) => Promise<void>,
573
+ ): Promise<void> {
574
+ let instancePromise = this.instancePromiseMap.get(instanceId)
575
+ if (instancePromise) {
576
+ return instancePromise
577
+ }
578
+
579
+ const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
580
+
581
+ instancePromise = fn(logger).finally(() => this.instancePromiseMap.delete(instanceId))
582
+ this.instancePromiseMap.set(instanceId, instancePromise)
583
+
584
+ return instancePromise
585
+ }
586
+
587
+ private getStatusByOperationType(): InstanceStatus {
588
+ switch (this.operation.type) {
589
+ case "update":
590
+ return "updating"
591
+ case "preview":
592
+ return "previewing"
593
+ case "recreate":
594
+ return "updating"
595
+ case "destroy":
596
+ return "destroying"
597
+ case "refresh":
598
+ return "refreshing"
599
+ }
600
+ }
601
+
602
+ private getInstanceDependencies(instance: InstanceModel): InstanceModel[] {
603
+ const dependencies: InstanceModel[] = []
604
+ const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
605
+
606
+ for (const inputs of Object.values(instanceInputs)) {
607
+ for (const input of inputs) {
608
+ const dependency = this.workset.getInstance(input.input.instanceId)
609
+
610
+ dependencies.push(dependency)
611
+ }
612
+ }
613
+
614
+ return dependencies
615
+ }
616
+
617
+ private persistStates = createAsyncBatcher(async (states: InstanceState[]) => {
618
+ this.logger.debug({ msg: "persisting states", count: states.length })
619
+
620
+ await this.stateBackend.putAffectedInstanceStates(
621
+ this.operation.projectId,
622
+ this.operation.id,
623
+ states,
624
+ )
625
+ })
626
+
627
+ private persistLogs = createAsyncBatcher(async (entries: LogEntry[]) => {
628
+ this.logger.trace({ msg: "persisting logs", count: entries.length })
629
+
630
+ await this.stateBackend.appendInstanceLogs(this.operation.id, entries)
631
+ })
632
+
633
+ private persistSecrets = createAsyncBatcher(
634
+ async (entries: [string, Record<string, string>][]) => {
635
+ this.logger.debug({ msg: "persisting secrets", count: entries.length })
636
+
637
+ // TODO: may be batched (and patched without reading)
638
+ for (const [instanceId, secrets] of entries) {
639
+ const existingSecrets = await this.secretBackend.get(this.operation.projectId, instanceId)
640
+
641
+ Object.assign(existingSecrets, secrets)
642
+
643
+ await this.secretBackend.set(this.operation.projectId, instanceId, existingSecrets)
644
+ }
645
+ },
646
+ )
647
+ }
@@ -0,0 +1 @@
1
+ export interface PreferencesBackend {}