@highstate/backend 0.9.15 → 0.9.16

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 (144) hide show
  1. package/dist/chunk-RCB4AFGD.js +159 -0
  2. package/dist/chunk-RCB4AFGD.js.map +1 -0
  3. package/dist/chunk-WHALQHEZ.js +2017 -0
  4. package/dist/chunk-WHALQHEZ.js.map +1 -0
  5. package/dist/highstate.manifest.json +3 -3
  6. package/dist/index.js +6158 -2178
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +47 -155
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +159 -41
  11. package/package.json +25 -7
  12. package/src/artifact/abstractions.ts +46 -0
  13. package/src/artifact/encryption.ts +69 -0
  14. package/src/artifact/factory.ts +36 -0
  15. package/src/artifact/index.ts +3 -0
  16. package/src/artifact/local.ts +142 -0
  17. package/src/business/api-key.ts +65 -0
  18. package/src/business/artifact.ts +288 -0
  19. package/src/business/backend-unlock.ts +10 -0
  20. package/src/business/index.ts +9 -0
  21. package/src/business/instance-lock.ts +124 -0
  22. package/src/business/instance-state.ts +292 -0
  23. package/src/business/operation.ts +251 -0
  24. package/src/business/project-unlock.ts +242 -0
  25. package/src/business/secret.ts +187 -0
  26. package/src/business/worker.ts +161 -0
  27. package/src/common/index.ts +2 -1
  28. package/src/common/performance.ts +44 -0
  29. package/src/common/tree.ts +33 -0
  30. package/src/common/utils.ts +40 -1
  31. package/src/config.ts +14 -10
  32. package/src/hotstate/abstractions.ts +48 -0
  33. package/src/hotstate/factory.ts +17 -0
  34. package/src/{secret → hotstate}/index.ts +1 -0
  35. package/src/hotstate/manager.ts +192 -0
  36. package/src/hotstate/memory.ts +100 -0
  37. package/src/hotstate/validation.ts +101 -0
  38. package/src/index.ts +2 -1
  39. package/src/library/abstractions.ts +10 -23
  40. package/src/library/factory.ts +2 -2
  41. package/src/library/local.ts +89 -102
  42. package/src/library/worker/evaluator.ts +9 -42
  43. package/src/library/worker/loader.lite.ts +41 -0
  44. package/src/library/worker/main.ts +14 -65
  45. package/src/library/worker/protocol.ts +8 -24
  46. package/src/lock/abstractions.ts +6 -0
  47. package/src/lock/factory.ts +15 -0
  48. package/src/{workspace → lock}/index.ts +1 -0
  49. package/src/lock/manager.ts +82 -0
  50. package/src/lock/memory.ts +19 -0
  51. package/src/orchestrator/manager.ts +129 -82
  52. package/src/orchestrator/operation-workset.ts +168 -77
  53. package/src/orchestrator/operation.ts +967 -284
  54. package/src/project/abstractions.ts +20 -7
  55. package/src/project/factory.ts +1 -1
  56. package/src/project/index.ts +0 -1
  57. package/src/project/local.ts +73 -17
  58. package/src/project/manager.ts +272 -131
  59. package/src/pubsub/abstractions.ts +13 -0
  60. package/src/pubsub/factory.ts +19 -0
  61. package/src/pubsub/index.ts +3 -0
  62. package/src/pubsub/local.ts +36 -0
  63. package/src/pubsub/manager.ts +100 -0
  64. package/src/pubsub/validation.ts +33 -0
  65. package/src/runner/abstractions.ts +135 -68
  66. package/src/runner/artifact-env.ts +160 -0
  67. package/src/runner/factory.ts +20 -5
  68. package/src/runner/force-abort.ts +117 -0
  69. package/src/runner/local.ts +281 -371
  70. package/src/{common → runner}/pulumi.ts +86 -37
  71. package/src/services.ts +193 -35
  72. package/src/shared/index.ts +3 -11
  73. package/src/shared/models/backend/index.ts +3 -0
  74. package/src/shared/models/backend/project.ts +63 -0
  75. package/src/shared/models/backend/unlock-method.ts +20 -0
  76. package/src/shared/models/base.ts +151 -0
  77. package/src/shared/models/errors.ts +5 -0
  78. package/src/shared/models/index.ts +4 -0
  79. package/src/shared/models/project/api-key.ts +62 -0
  80. package/src/shared/models/project/artifact.ts +113 -0
  81. package/src/shared/models/project/component.ts +45 -0
  82. package/src/shared/models/project/index.ts +14 -0
  83. package/src/shared/{project.ts → models/project/instance.ts} +12 -0
  84. package/src/shared/models/project/lock.ts +91 -0
  85. package/src/shared/{operation.ts → models/project/operation.ts} +28 -7
  86. package/src/shared/models/project/page.ts +57 -0
  87. package/src/shared/models/project/secret.ts +112 -0
  88. package/src/shared/models/project/service-account.ts +22 -0
  89. package/src/shared/models/project/state.ts +432 -0
  90. package/src/shared/models/project/terminal.ts +99 -0
  91. package/src/shared/models/project/trigger.ts +56 -0
  92. package/src/shared/models/project/unlock-method.ts +31 -0
  93. package/src/shared/models/project/worker.ts +105 -0
  94. package/src/shared/resolvers/graph-resolver.ts +28 -0
  95. package/src/shared/resolvers/index.ts +5 -0
  96. package/src/shared/resolvers/input-hash.ts +53 -15
  97. package/src/shared/resolvers/input.ts +1 -9
  98. package/src/shared/resolvers/registry.ts +3 -2
  99. package/src/shared/resolvers/state.ts +2 -2
  100. package/src/shared/resolvers/validation.ts +61 -20
  101. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  102. package/src/shared/utils/hash.ts +6 -0
  103. package/src/shared/utils/index.ts +3 -0
  104. package/src/shared/utils/promise-tracker.ts +23 -0
  105. package/src/state/abstractions.ts +330 -101
  106. package/src/state/encryption.ts +59 -0
  107. package/src/state/factory.ts +3 -5
  108. package/src/state/index.ts +3 -0
  109. package/src/state/keyring.ts +22 -0
  110. package/src/state/local/backend.ts +299 -0
  111. package/src/state/local/collection.ts +342 -0
  112. package/src/state/local/index.ts +2 -0
  113. package/src/state/manager.ts +804 -18
  114. package/src/state/repository/index.ts +2 -0
  115. package/src/state/repository/repository.index.ts +193 -0
  116. package/src/state/repository/repository.ts +458 -0
  117. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  118. package/src/terminal/docker.ts +18 -14
  119. package/src/terminal/factory.ts +3 -3
  120. package/src/terminal/index.ts +1 -1
  121. package/src/terminal/manager.ts +131 -79
  122. package/src/terminal/run.sh.ts +21 -11
  123. package/src/worker/abstractions.ts +42 -0
  124. package/src/worker/docker.ts +83 -0
  125. package/src/worker/factory.ts +20 -0
  126. package/src/worker/index.ts +3 -0
  127. package/src/worker/manager.ts +139 -0
  128. package/dist/chunk-KTGKNSKM.js +0 -979
  129. package/dist/chunk-KTGKNSKM.js.map +0 -1
  130. package/dist/chunk-WXDYCRTT.js +0 -234
  131. package/dist/chunk-WXDYCRTT.js.map +0 -1
  132. package/src/library/worker/loader.ts +0 -114
  133. package/src/preferences/shared.ts +0 -1
  134. package/src/project/lock.ts +0 -39
  135. package/src/secret/abstractions.ts +0 -59
  136. package/src/secret/factory.ts +0 -22
  137. package/src/secret/local.ts +0 -152
  138. package/src/shared/state.ts +0 -247
  139. package/src/shared/terminal.ts +0 -14
  140. package/src/state/local.ts +0 -612
  141. package/src/workspace/abstractions.ts +0 -41
  142. package/src/workspace/factory.ts +0 -14
  143. package/src/workspace/local.ts +0 -54
  144. /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
@@ -0,0 +1,82 @@
1
+ import type { LockBackend } from "./abstractions"
2
+ import { join } from "remeda"
3
+
4
+ export type LockKeyMap = {
5
+ /**
6
+ * Lock for a specific project.
7
+ */
8
+ project: [projectId: string]
9
+
10
+ /**
11
+ * Lock for a specific instance within a project.
12
+ */
13
+ instance: [projectId: string, instanceId: string]
14
+
15
+ /**
16
+ * Lock for a specific instance lock within a project.
17
+ */
18
+ "instance-lock": [projectId: string, instanceId: string]
19
+
20
+ /**
21
+ * Lock for a specific instance state within a project.
22
+ */
23
+ "instance-state": [projectId: string, instanceId: string]
24
+
25
+ /**
26
+ * Lock for a specific artifact within a project.
27
+ */
28
+ artifact: [projectId: string, artifactId: string]
29
+
30
+ /**
31
+ * Lock for a specific artifact hash within a project.
32
+ */
33
+ "artifact-hash": [projectId: string, hash: string]
34
+
35
+ /**
36
+ * Lock for a specific operation within a project.
37
+ */
38
+ operation: [projectId: string, operationId: string]
39
+
40
+ /**
41
+ * Lock for a specific api key within a project.
42
+ */
43
+ "api-key": [projectId: string, key: string]
44
+
45
+ /**
46
+ * Lock for a specific worker within a project.
47
+ */
48
+ worker: [workerId: string]
49
+
50
+ /**
51
+ * Lock for a specific worker image within a project.
52
+ */
53
+ "worker-image": [projectId: string, image: string]
54
+ }
55
+
56
+ export type LockKey = Readonly<
57
+ {
58
+ [K in keyof LockKeyMap]: [type: K, ...LockKeyMap[K]]
59
+ }[keyof LockKeyMap]
60
+ >
61
+
62
+ export class LockManager {
63
+ constructor(private readonly lockBackend: LockBackend) {}
64
+
65
+ /**
66
+ * Acquires locks for the given keys and executes the function.
67
+ *
68
+ * This method provides a clean interface for acquiring distributed locks across
69
+ * multiple keys and ensures they are properly released after execution.
70
+ *
71
+ * @param keys The keys to acquire locks for.
72
+ * @param fn The function to execute while holding the locks.
73
+ * @returns The result of the executed function.
74
+ */
75
+ public async acquire<T>(keys: LockKey | LockKey[], fn: () => Promise<T> | T): Promise<T> {
76
+ if (typeof keys[0] === "string") {
77
+ return this.lockBackend.acquire([keys.join(":")], fn)
78
+ }
79
+
80
+ return this.lockBackend.acquire(keys.map(join(":")), fn)
81
+ }
82
+ }
@@ -0,0 +1,19 @@
1
+ import type { LockBackend } from "./abstractions"
2
+ import type { BetterLock as BetterLockType } from "better-lock/dist/better_lock"
3
+ import { BetterLock } from "better-lock"
4
+
5
+ export class MemoryLockBackend implements LockBackend {
6
+ private readonly lock: BetterLockType
7
+
8
+ private constructor() {
9
+ this.lock = new BetterLock()
10
+ }
11
+
12
+ async acquire<T>(keys: string[], fn: () => Promise<T> | T): Promise<T> {
13
+ return this.lock.acquire(keys, fn)
14
+ }
15
+
16
+ static create(): LockBackend {
17
+ return new MemoryLockBackend()
18
+ }
19
+ }
@@ -1,76 +1,54 @@
1
1
  import type { LibraryBackend } from "../library"
2
- import type { ProjectBackend, ProjectLockManager } from "../project"
2
+ import type { ProjectBackend } from "../project"
3
3
  import type { RunnerBackend } from "../runner"
4
- import type { SecretBackend } from "../secret"
5
- import type { StateBackend, StateManager } from "../state"
4
+ import type { StateManager } from "../state"
5
+ import type { ArtifactService } from "../business/artifact"
6
6
  import type { Logger } from "pino"
7
- import { EventEmitter, on } from "node:events"
8
- import { uuidv7 } from "uuidv7"
9
- import { type ProjectOperation, type ProjectOperationRequest } from "../shared"
7
+ import type {
8
+ InstanceLockService,
9
+ OperationService,
10
+ SecretService,
11
+ ProjectUnlockService,
12
+ InstanceStateService,
13
+ } from "../business"
14
+ import type { PubSubManager } from "../pubsub"
15
+ import { v7 as uuidv7 } from "uuid"
16
+ import { isFinalOperationStatus, type Operation, type OperationRequest } from "../shared"
10
17
  import { RuntimeOperation } from "./operation"
11
18
 
12
- export type OperationEvents = Record<string, [ProjectOperation]>
13
- export type InstanceLogsEvents = Record<string, [string]>
14
-
15
19
  export class OperationManager {
16
20
  constructor(
17
21
  private readonly runnerBackend: RunnerBackend,
18
- private readonly stateBackend: StateBackend,
19
22
  private readonly libraryBackend: LibraryBackend,
20
23
  private readonly projectBackend: ProjectBackend,
21
- private readonly secretBackend: SecretBackend,
22
- private readonly projectLockManager: ProjectLockManager,
24
+ private readonly artifactManager: ArtifactService,
23
25
  private readonly stateManager: StateManager,
26
+ private readonly instanceLockService: InstanceLockService,
27
+ private readonly stateUnlockService: ProjectUnlockService,
28
+ private readonly operationService: OperationService,
29
+ private readonly secretService: SecretService,
30
+ private readonly instanceStateService: InstanceStateService,
31
+ private readonly pubsubManager: PubSubManager,
24
32
  private readonly logger: Logger,
25
- ) {}
26
-
27
- private readonly operationEE = new EventEmitter<OperationEvents>()
28
- private readonly instanceLogsEE = new EventEmitter<InstanceLogsEvents>()
29
-
30
- private readonly runtimeOperations = new Map<string, RuntimeOperation>()
31
-
32
- /**
33
- * Watches for all project operations in the project.
34
- *
35
- * @param projectId The project ID to watch.
36
- * @param signal The signal to abort the operation.
37
- */
38
- public async *watchOperations(
39
- projectId: string,
40
- signal?: AbortSignal,
41
- ): AsyncIterable<ProjectOperation> {
42
- for await (const [operation] of on(this.operationEE, projectId, { signal })) {
43
- yield operation
44
- }
33
+ ) {
34
+ this.stateUnlockService.registerUnlockTask(
35
+ //
36
+ "process-lost-operations",
37
+ projectId => this.processLostOperations(projectId),
38
+ )
45
39
  }
46
40
 
47
- /**
48
- * Watches for logs of the instance in the operation.
49
- *
50
- * @param operationId The operation ID to watch.
51
- * @param instanceId The instance ID to watch.
52
- * @param signal The signal to abort the operation.
53
- */
54
- async *watchInstanceLogs(
55
- operationId: string,
56
- instanceId: string,
57
- signal?: AbortSignal,
58
- ): AsyncIterable<string> {
59
- const eventKey = `${operationId}/${instanceId}`
60
- for await (const [log] of on(this.instanceLogsEE, eventKey, { signal })) {
61
- yield log
62
- }
63
- }
41
+ private readonly runtimeOperations = new Map<string, RuntimeOperation>()
64
42
 
65
43
  /**
66
44
  * Launches the project operation.
67
45
  *
68
46
  * @param request The operation request to launch.
69
47
  */
70
- async launch(request: ProjectOperationRequest): Promise<ProjectOperation> {
71
- const operation: ProjectOperation = {
48
+ async launch(request: OperationRequest): Promise<Operation> {
49
+ const operation: Operation = {
72
50
  id: uuidv7(),
73
- projectId: request.projectId,
51
+ meta: {},
74
52
  type: request.type,
75
53
  requestedInstanceIds: request.instanceIds,
76
54
  instanceIdsToUpdate: [],
@@ -94,9 +72,9 @@ export class OperationManager {
94
72
 
95
73
  this.logger.info({ operation }, "launching operation")
96
74
 
97
- await this.stateBackend.putOperation(operation)
75
+ await this.operationService.updateOperation(request.projectId, operation)
98
76
 
99
- this.startOperation(operation)
77
+ this.startOperation(request.projectId, operation)
100
78
 
101
79
  return operation
102
80
  }
@@ -112,59 +90,128 @@ export class OperationManager {
112
90
  }
113
91
  }
114
92
 
115
- private startOperation(operation: ProjectOperation): void {
93
+ cancelInstance(operationId: string, instanceId: string): void {
94
+ const runtimeOperation = this.runtimeOperations.get(operationId)
95
+ if (runtimeOperation) {
96
+ runtimeOperation.cancelInstance(instanceId)
97
+ }
98
+ }
99
+
100
+ private startOperation(projectId: string, operation: Operation): void {
116
101
  const runtimeOperation = new RuntimeOperation(
102
+ projectId,
117
103
  operation,
118
104
  this.runnerBackend,
119
- this.stateBackend,
120
105
  this.libraryBackend,
121
106
  this.projectBackend,
122
- this.secretBackend,
123
- this.projectLockManager.getLock(operation.projectId),
107
+ this.artifactManager,
124
108
  this.stateManager,
125
- this.operationEE,
126
- this.instanceLogsEE,
127
- this.logger.child({ service: "RuntimeOperation", operationId: operation.id }),
109
+ this.instanceLockService,
110
+ this.operationService,
111
+ this.secretService,
112
+ this.instanceStateService,
113
+ this.pubsubManager,
114
+ this.logger.child({ operationId: operation.id }),
128
115
  )
129
116
 
130
117
  this.runtimeOperations.set(operation.id, runtimeOperation)
131
118
  void runtimeOperation.operateSafe().finally(() => this.runtimeOperations.delete(operation.id))
132
119
  }
133
120
 
134
- static async create(
121
+ private async processLostOperations(projectId: string): Promise<void> {
122
+ const activeOperations = await this.stateManager
123
+ .getActiveOperationIndexRepository(projectId)
124
+ .getAllItems()
125
+
126
+ for (const operation of activeOperations) {
127
+ if (isFinalOperationStatus(operation.status)) {
128
+ this.logger.warn(
129
+ { projectId, operationId: operation.id },
130
+ "operation is in final state but still marked as active, removing from index",
131
+ )
132
+
133
+ await this.stateManager
134
+ .getActiveOperationIndexRepository(projectId)
135
+ .indexRepository.delete(operation.id)
136
+ continue
137
+ }
138
+
139
+ const errorMessagePrefix = operation.error ? `${operation.error}\n\n` : ""
140
+
141
+ operation.status = "failed"
142
+ operation.error = `${errorMessagePrefix}Operation was unexpectedly interrupted. Please restart it.`
143
+
144
+ await this.operationService.updateOperation(projectId, operation)
145
+ }
146
+
147
+ // unlock instances that were locked by lost operations
148
+ const activeOperationIds = new Set(activeOperations.map(op => op.id))
149
+
150
+ const allLocks = await this.stateManager.getInstanceLockRepository(projectId).getAllItems()
151
+ const lockIdsToRemvoe: string[] = []
152
+
153
+ // clean up unexpected operation locks
154
+ for (const lock of allLocks) {
155
+ if (lock.spec.type !== "operation") {
156
+ continue
157
+ }
158
+
159
+ if (activeOperationIds.has(lock.spec.operationId)) {
160
+ // remove locks for lost operation which is expected
161
+ lockIdsToRemvoe.push(lock.id)
162
+ continue
163
+ }
164
+
165
+ // unexpected operation lock found, also remove it
166
+ this.logger.warn(
167
+ { projectId, lockId: lock.id },
168
+ `unexpected operation lock found for completed operation "${lock.spec.operationId}", removing lock`,
169
+ )
170
+
171
+ lockIdsToRemvoe.push(lock.id)
172
+ }
173
+
174
+ if (lockIdsToRemvoe.length > 0) {
175
+ await this.instanceLockService.unlockInstancesUnconditionally(projectId, lockIdsToRemvoe)
176
+ }
177
+
178
+ this.logger.debug(
179
+ { projectId, operationCount: activeOperations.length },
180
+ `processed %s lost operations for project "%s"`,
181
+ activeOperations.length,
182
+ projectId,
183
+ )
184
+ }
185
+
186
+ static create(
135
187
  runnerBackend: RunnerBackend,
136
- stateBackend: StateBackend,
137
188
  libraryBackend: LibraryBackend,
138
189
  projectBackend: ProjectBackend,
139
- secretBackend: SecretBackend,
140
- projectLockManager: ProjectLockManager,
190
+ artifactManager: ArtifactService,
141
191
  stateManager: StateManager,
192
+ instanceLockService: InstanceLockService,
193
+ stateUnlockService: ProjectUnlockService,
194
+ operationService: OperationService,
195
+ secretService: SecretService,
196
+ instanceService: InstanceStateService,
197
+ pubsubManager: PubSubManager,
142
198
  logger: Logger,
143
- ) {
199
+ ): OperationManager {
144
200
  const operator = new OperationManager(
145
201
  runnerBackend,
146
- stateBackend,
147
202
  libraryBackend,
148
203
  projectBackend,
149
- secretBackend,
150
- projectLockManager,
204
+ artifactManager,
151
205
  stateManager,
206
+ instanceLockService,
207
+ stateUnlockService,
208
+ operationService,
209
+ secretService,
210
+ instanceService,
211
+ pubsubManager,
152
212
  logger.child({ service: "OperationManager" }),
153
213
  )
154
214
 
155
- // relaunch all active operations
156
- const activeOperations = await stateBackend.getActiveOperations()
157
-
158
- for (const operation of activeOperations) {
159
- logger.info({ msg: "relaunching operation", operationId: operation.id })
160
-
161
- // operator.startOperation(operation)
162
-
163
- // cancel the operation for now
164
- operation.status = "cancelled"
165
- await stateBackend.putOperation(operation)
166
- }
167
-
168
215
  return operator
169
216
  }
170
217
  }