@highstate/backend 0.9.14 → 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.
- package/dist/chunk-RCB4AFGD.js +159 -0
- package/dist/chunk-RCB4AFGD.js.map +1 -0
- package/dist/chunk-WHALQHEZ.js +2017 -0
- package/dist/chunk-WHALQHEZ.js.map +1 -0
- package/dist/highstate.manifest.json +3 -3
- package/dist/index.js +6146 -2174
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +51 -159
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +159 -43
- package/package.json +25 -7
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +69 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +142 -0
- package/src/business/api-key.ts +65 -0
- package/src/business/artifact.ts +288 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +9 -0
- package/src/business/instance-lock.ts +124 -0
- package/src/business/instance-state.ts +292 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +242 -0
- package/src/business/secret.ts +187 -0
- package/src/business/worker.ts +161 -0
- package/src/common/index.ts +2 -1
- package/src/common/performance.ts +44 -0
- package/src/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +14 -10
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +101 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +10 -23
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +89 -102
- package/src/library/worker/evaluator.ts +14 -47
- package/src/library/worker/loader.lite.ts +41 -0
- package/src/library/worker/main.ts +14 -65
- package/src/library/worker/protocol.ts +8 -24
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/{workspace → lock}/index.ts +1 -0
- package/src/lock/manager.ts +82 -0
- package/src/lock/memory.ts +19 -0
- package/src/orchestrator/manager.ts +131 -82
- package/src/orchestrator/operation-workset.ts +188 -77
- package/src/orchestrator/operation.ts +975 -284
- package/src/project/abstractions.ts +20 -7
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +0 -1
- package/src/project/local.ts +73 -17
- package/src/project/manager.ts +272 -131
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/pubsub/index.ts +3 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +100 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +135 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +281 -372
- package/src/{common → runner}/pulumi.ts +86 -37
- package/src/services.ts +193 -35
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/models/backend/project.ts +63 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +151 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +62 -0
- package/src/shared/models/project/artifact.ts +113 -0
- package/src/shared/models/project/component.ts +45 -0
- package/src/shared/models/project/index.ts +14 -0
- package/src/shared/{project.ts → models/project/instance.ts} +12 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +43 -8
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +112 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +432 -0
- package/src/shared/models/project/terminal.ts +99 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +31 -0
- package/src/shared/models/project/worker.ts +105 -0
- package/src/shared/resolvers/graph-resolver.ts +74 -13
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +1 -9
- package/src/shared/resolvers/registry.ts +7 -2
- package/src/shared/resolvers/state.ts +12 -0
- package/src/shared/resolvers/validation.ts +61 -20
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +3 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +330 -101
- package/src/state/encryption.ts +59 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +3 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +299 -0
- package/src/state/local/collection.ts +342 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +804 -18
- package/src/state/repository/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +458 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +134 -80
- package/src/terminal/run.sh.ts +22 -10
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +139 -0
- package/dist/chunk-C2TJAQAD.js +0 -937
- package/dist/chunk-C2TJAQAD.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/state.ts +0 -270
- package/src/shared/terminal.ts +0 -13
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- package/src/workspace/local.ts +0 -54
- /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
package/src/project/manager.ts
CHANGED
|
@@ -1,75 +1,43 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { StateManager } from "../state"
|
|
2
2
|
import type { ProjectBackend } from "./abstractions"
|
|
3
3
|
import type { Logger } from "pino"
|
|
4
|
-
import type { LibraryBackend } from "../library"
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
4
|
+
import type { InstanceEvaluationResult, LibraryBackend } from "../library"
|
|
5
|
+
import type { InstanceLockService } from "../business"
|
|
6
|
+
import type { PubSubManager } from "../pubsub"
|
|
7
|
+
import { randomUUID } from "node:crypto"
|
|
7
8
|
import { isUnitModel, type InstanceModel } from "@highstate/contract"
|
|
8
9
|
import {
|
|
9
10
|
type InputResolverNode,
|
|
10
11
|
type InputHashNode,
|
|
11
12
|
type InstanceModelPatch,
|
|
12
|
-
type LibraryUpdate,
|
|
13
|
-
createInstanceState,
|
|
14
13
|
type CompositeInstance,
|
|
15
14
|
type ResolvedInstanceInput,
|
|
16
15
|
type HubModel,
|
|
16
|
+
type LibraryUpdate,
|
|
17
17
|
InputResolver,
|
|
18
18
|
InputHashResolver,
|
|
19
19
|
} from "../shared"
|
|
20
|
-
|
|
21
|
-
type CompositeInstanceEvent =
|
|
22
|
-
| {
|
|
23
|
-
type: "evaluation-started"
|
|
24
|
-
instanceId: string
|
|
25
|
-
}
|
|
26
|
-
| {
|
|
27
|
-
type: "updated"
|
|
28
|
-
instance: CompositeInstance
|
|
29
|
-
}
|
|
30
|
-
| {
|
|
31
|
-
type: "deleted"
|
|
32
|
-
instanceId: string
|
|
33
|
-
}
|
|
34
|
-
| {
|
|
35
|
-
type: "failed"
|
|
36
|
-
instanceId: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type CompositeInstanceEvents = {
|
|
40
|
-
[K in string]: [CompositeInstanceEvent]
|
|
41
|
-
}
|
|
20
|
+
import { renderTree, type TreeNode } from "../common"
|
|
42
21
|
|
|
43
22
|
export type FullProjectModel = {
|
|
23
|
+
libraryId: string
|
|
44
24
|
instances: InstanceModel[]
|
|
45
25
|
hubs: HubModel[]
|
|
46
26
|
compositeInstances: CompositeInstance[]
|
|
47
27
|
}
|
|
48
28
|
|
|
49
29
|
export class ProjectManager {
|
|
30
|
+
private readonly watchedLibraries = new Set<string>()
|
|
31
|
+
private readonly libraryWatchers = new Map<string, AbortController>()
|
|
32
|
+
|
|
50
33
|
private constructor(
|
|
51
34
|
private readonly projectBackend: ProjectBackend,
|
|
52
|
-
private readonly stateBackend: StateBackend,
|
|
53
35
|
private readonly libraryBackend: LibraryBackend,
|
|
54
|
-
private readonly projectLockManager: ProjectLockManager,
|
|
55
36
|
private readonly stateManager: StateManager,
|
|
37
|
+
private readonly instanceLockService: InstanceLockService,
|
|
38
|
+
private readonly pubsubManager: PubSubManager,
|
|
56
39
|
private readonly logger: Logger,
|
|
57
|
-
) {
|
|
58
|
-
void this.watchLibraryChanges()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private readonly compositeInstanceEE = new EventEmitter<CompositeInstanceEvents>()
|
|
62
|
-
|
|
63
|
-
async *watchCompositeInstances(
|
|
64
|
-
projectId: string,
|
|
65
|
-
signal?: AbortSignal,
|
|
66
|
-
): AsyncIterable<CompositeInstanceEvent> {
|
|
67
|
-
for await (const [children] of on(this.compositeInstanceEE, projectId, {
|
|
68
|
-
signal,
|
|
69
|
-
})) {
|
|
70
|
-
yield children
|
|
71
|
-
}
|
|
72
|
-
}
|
|
40
|
+
) {}
|
|
73
41
|
|
|
74
42
|
/**
|
|
75
43
|
* Loads the full info of a project, including instances, hubs, and composite instances.
|
|
@@ -79,13 +47,22 @@ export class ProjectManager {
|
|
|
79
47
|
* @param projectId The ID of the project to load.
|
|
80
48
|
*/
|
|
81
49
|
async getProject(projectId: string): Promise<FullProjectModel> {
|
|
82
|
-
const [
|
|
50
|
+
const [projectInfo, project] = await Promise.all([
|
|
51
|
+
this.projectBackend.getProjectInfo(projectId),
|
|
83
52
|
this.projectBackend.getProject(projectId),
|
|
84
|
-
this.stateBackend.getCompositeInstances(projectId),
|
|
85
|
-
this.libraryBackend.loadLibrary(),
|
|
86
53
|
])
|
|
87
54
|
|
|
88
|
-
|
|
55
|
+
// Ensure we're watching this library for changes
|
|
56
|
+
this.ensureLibraryWatched(projectInfo.libraryId)
|
|
57
|
+
|
|
58
|
+
const [compositeInstances, library] = await Promise.all([
|
|
59
|
+
this.stateManager.getCompositeInstanceRepository(projectId).getAllItems(),
|
|
60
|
+
this.libraryBackend.loadLibrary(projectInfo.libraryId),
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const filteredInstances = project.instances.filter(
|
|
64
|
+
instance => instance.type in library.components,
|
|
65
|
+
)
|
|
89
66
|
const filteredCompositeInstances = compositeInstances
|
|
90
67
|
.filter(instance => instance.instance.type in library.components)
|
|
91
68
|
.map(instance => ({
|
|
@@ -94,8 +71,9 @@ export class ProjectManager {
|
|
|
94
71
|
}))
|
|
95
72
|
|
|
96
73
|
return {
|
|
74
|
+
libraryId: projectInfo.libraryId,
|
|
97
75
|
instances: filteredInstances,
|
|
98
|
-
hubs,
|
|
76
|
+
hubs: project.hubs,
|
|
99
77
|
compositeInstances: filteredCompositeInstances,
|
|
100
78
|
}
|
|
101
79
|
}
|
|
@@ -113,7 +91,7 @@ export class ProjectManager {
|
|
|
113
91
|
patch: InstanceModelPatch,
|
|
114
92
|
): Promise<InstanceModel> {
|
|
115
93
|
const instance = await this.projectBackend.updateInstance(projectId, instanceId, patch)
|
|
116
|
-
await this.evaluateChangedCompositeInstances(projectId)
|
|
94
|
+
// await this.evaluateChangedCompositeInstances(projectId)
|
|
117
95
|
|
|
118
96
|
return instance
|
|
119
97
|
}
|
|
@@ -132,10 +110,18 @@ export class ProjectManager {
|
|
|
132
110
|
async deleteInstance(projectId: string, instanceId: string): Promise<void> {
|
|
133
111
|
await Promise.all([
|
|
134
112
|
this.projectBackend.deleteInstance(projectId, instanceId),
|
|
135
|
-
this.
|
|
113
|
+
this.stateManager.getCompositeInstanceRepository(projectId).delete(instanceId),
|
|
136
114
|
])
|
|
137
115
|
}
|
|
138
116
|
|
|
117
|
+
async createManyNodes(
|
|
118
|
+
projectId: string,
|
|
119
|
+
instances: InstanceModel[],
|
|
120
|
+
hubs: HubModel[],
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
await this.projectBackend.createManyNodes(projectId, instances, hubs)
|
|
123
|
+
}
|
|
124
|
+
|
|
139
125
|
private async evaluateChangedCompositeInstances(projectId: string): Promise<void> {
|
|
140
126
|
const { inputHashResolver, instances, library, evaluatedInputHashes } =
|
|
141
127
|
await this.prepareResolvers(projectId)
|
|
@@ -152,22 +138,51 @@ export class ProjectManager {
|
|
|
152
138
|
)
|
|
153
139
|
.map(instance => instance.id)
|
|
154
140
|
|
|
155
|
-
await this.
|
|
141
|
+
await this.instanceLockService.tryLockInstances(
|
|
142
|
+
projectId,
|
|
143
|
+
instanceIds,
|
|
144
|
+
{
|
|
145
|
+
title: "Composite Instance Evaluation",
|
|
146
|
+
description: "This instance is now being evaluated",
|
|
147
|
+
icon: "logos:typescript-icon-round",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: "evaluation",
|
|
151
|
+
evaluationId: randomUUID(),
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
156
|
this.logger.debug({ instanceIds }, "evaluating composite instances")
|
|
157
157
|
|
|
158
158
|
for (const instanceId of instanceIds) {
|
|
159
|
-
this.
|
|
159
|
+
void this.pubsubManager.publish(`instance:state:patch:${projectId}`, {
|
|
160
|
+
patch: {
|
|
161
|
+
id: instanceId,
|
|
162
|
+
evaluationStatus: {
|
|
163
|
+
status: "evaluating",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
})
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
const [
|
|
163
|
-
{ instances, resolvedInputs, stateMap, inputHashResolver },
|
|
164
|
-
|
|
170
|
+
{ instances, resolvedInputs, stateMap, inputHashResolver, libraryId },
|
|
171
|
+
existingCompositeInstances,
|
|
165
172
|
] = await Promise.all([
|
|
166
173
|
this.prepareResolvers(projectId),
|
|
167
|
-
this.
|
|
174
|
+
this.stateManager.getCompositeInstanceRepository(projectId).getAllRecord(),
|
|
168
175
|
])
|
|
169
176
|
|
|
177
|
+
// Get child instance IDs for the instances being re-evaluated
|
|
178
|
+
const oldChildInstanceIds = new Set(
|
|
179
|
+
existingCompositeInstances
|
|
180
|
+
.filter(ci => instanceIds.includes(ci.instance.id))
|
|
181
|
+
.flatMap(ci => ci.childCompositeInstanceIds ?? []),
|
|
182
|
+
)
|
|
183
|
+
|
|
170
184
|
const results = await this.libraryBackend.evaluateCompositeInstances(
|
|
185
|
+
libraryId,
|
|
171
186
|
instances,
|
|
172
187
|
resolvedInputs,
|
|
173
188
|
instanceIds,
|
|
@@ -175,9 +190,11 @@ export class ProjectManager {
|
|
|
175
190
|
|
|
176
191
|
const newStates = results.map(result => {
|
|
177
192
|
const existingState = stateMap.get(result.instanceId)
|
|
178
|
-
const newState = existingState ??
|
|
193
|
+
const newState = existingState ?? { id: result.instanceId }
|
|
179
194
|
|
|
180
|
-
newState.
|
|
195
|
+
newState.evaluationStatus = result.success
|
|
196
|
+
? { status: "evaluated", message: ProjectManager.createSuccessEvaluationMessage(result) }
|
|
197
|
+
: { status: "error", message: result.error }
|
|
181
198
|
|
|
182
199
|
return newState
|
|
183
200
|
})
|
|
@@ -190,76 +207,81 @@ export class ProjectManager {
|
|
|
190
207
|
.flatMap(result =>
|
|
191
208
|
result.compositeInstances.map(instance => ({
|
|
192
209
|
...instance,
|
|
210
|
+
id: instance.instance.id,
|
|
193
211
|
inputHash:
|
|
194
212
|
// only store inputHash for top-level composite instances
|
|
195
213
|
instance.instance.id === result.instanceId
|
|
196
214
|
? inputHashResolver.requireOutput(instance.instance.id).inputHash
|
|
197
215
|
: "",
|
|
216
|
+
// only store childCompositeInstanceIds for top-level composite instances
|
|
217
|
+
childCompositeInstanceIds:
|
|
218
|
+
instance.instance.id === result.instanceId
|
|
219
|
+
? result.compositeInstances
|
|
220
|
+
.filter(ci => ci.instance.id !== result.instanceId)
|
|
221
|
+
.map(ci => ci.instance.id)
|
|
222
|
+
: undefined,
|
|
198
223
|
})),
|
|
199
224
|
)
|
|
200
225
|
|
|
201
|
-
const
|
|
226
|
+
const newChildInstanceIds = new Set(
|
|
202
227
|
results
|
|
203
228
|
.filter(result => result.success)
|
|
204
|
-
.
|
|
205
|
-
result.instanceId,
|
|
229
|
+
.flatMap(result =>
|
|
206
230
|
result.compositeInstances
|
|
207
231
|
.filter(instance => instance.instance.id !== result.instanceId)
|
|
208
232
|
.map(instance => instance.instance.id),
|
|
209
|
-
|
|
233
|
+
),
|
|
210
234
|
)
|
|
211
235
|
|
|
212
|
-
const deletedCompositeInstanceIds = new Set(
|
|
213
|
-
Object.values(topLevelCompositeChildrenIds).flat(),
|
|
214
|
-
)
|
|
236
|
+
const deletedCompositeInstanceIds = new Set<string>()
|
|
215
237
|
|
|
216
|
-
|
|
217
|
-
|
|
238
|
+
// Find child instances that were deleted (existed before but don't exist now)
|
|
239
|
+
for (const oldChildId of oldChildInstanceIds) {
|
|
240
|
+
if (!newChildInstanceIds.has(oldChildId)) {
|
|
241
|
+
deletedCompositeInstanceIds.add(oldChildId)
|
|
242
|
+
}
|
|
218
243
|
}
|
|
219
244
|
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Array.from(deletedCompositeInstanceIds),
|
|
224
|
-
),
|
|
225
|
-
this.stateBackend.putTopLevelCompositeChildrenIds(
|
|
226
|
-
projectId,
|
|
227
|
-
newTopLevelCompositeChildrenIds,
|
|
228
|
-
),
|
|
229
|
-
])
|
|
245
|
+
await this.stateManager
|
|
246
|
+
.getCompositeInstanceRepository(projectId)
|
|
247
|
+
.deleteMany(Array.from(deletedCompositeInstanceIds))
|
|
230
248
|
|
|
231
249
|
for (const state of newStates) {
|
|
232
|
-
this.
|
|
250
|
+
void this.pubsubManager.publish(`instance:state:patch:${projectId}`, { patch: state })
|
|
233
251
|
|
|
234
|
-
if (state.
|
|
252
|
+
if (state.evaluationStatus?.status === "error") {
|
|
235
253
|
this.logger.error(
|
|
236
|
-
{ projectId, instanceId: state.id
|
|
237
|
-
|
|
254
|
+
{ projectId, instanceId: state.id },
|
|
255
|
+
`evaluation error for instance "%s": %s`,
|
|
256
|
+
state.evaluationStatus.message,
|
|
238
257
|
)
|
|
239
|
-
|
|
240
|
-
this.compositeInstanceEE.emit(projectId, {
|
|
241
|
-
type: "failed",
|
|
242
|
-
instanceId: state.id,
|
|
243
|
-
})
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
for (const instance of compositeInstances) {
|
|
248
|
-
this.
|
|
262
|
+
void this.pubsubManager.publish(`composite-instance:${projectId}`, {
|
|
263
|
+
type: "updated",
|
|
264
|
+
instance,
|
|
265
|
+
})
|
|
249
266
|
}
|
|
250
267
|
|
|
251
268
|
for (const instanceId of deletedCompositeInstanceIds) {
|
|
252
|
-
this.
|
|
269
|
+
void this.pubsubManager.publish(`composite-instance:${projectId}`, {
|
|
270
|
+
type: "deleted",
|
|
271
|
+
instanceId,
|
|
272
|
+
})
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
const promises: Promise<void>[] = []
|
|
256
276
|
|
|
257
277
|
if (newStates.length > 0) {
|
|
258
|
-
promises.push(this.
|
|
278
|
+
promises.push(this.stateManager.getInstanceStateRepository(projectId).putMany(newStates))
|
|
259
279
|
}
|
|
260
280
|
|
|
261
281
|
if (compositeInstances.length > 0) {
|
|
262
|
-
promises.push(
|
|
282
|
+
promises.push(
|
|
283
|
+
this.stateManager.getCompositeInstanceRepository(projectId).putMany(compositeInstances),
|
|
284
|
+
)
|
|
263
285
|
}
|
|
264
286
|
|
|
265
287
|
this.logger.info(
|
|
@@ -274,17 +296,21 @@ export class ProjectManager {
|
|
|
274
296
|
)
|
|
275
297
|
|
|
276
298
|
await Promise.all(promises)
|
|
277
|
-
}
|
|
299
|
+
} finally {
|
|
300
|
+
// TODO: implement detecting of lost evaluation locks
|
|
301
|
+
await this.instanceLockService.unlockInstancesUnconditionally(projectId, instanceIds)
|
|
302
|
+
}
|
|
278
303
|
}
|
|
279
304
|
|
|
280
305
|
private async prepareResolvers(projectId: string) {
|
|
281
|
-
const [{ instances, hubs }, states
|
|
306
|
+
const [projectInfo, { instances, hubs }, states] = await Promise.all([
|
|
307
|
+
this.projectBackend.getProjectInfo(projectId),
|
|
282
308
|
this.projectBackend.getProject(projectId),
|
|
283
|
-
this.
|
|
284
|
-
this.libraryBackend.loadLibrary(),
|
|
285
|
-
this.stateBackend.getCompositeInstanceInputHashes(projectId),
|
|
309
|
+
this.stateManager.getInstanceStateRepository(projectId).getAllItems(),
|
|
286
310
|
])
|
|
287
311
|
|
|
312
|
+
const library = await this.libraryBackend.loadLibrary(projectInfo.libraryId)
|
|
313
|
+
|
|
288
314
|
const filteredInstances = instances.filter(instance => instance.type in library.components)
|
|
289
315
|
const stateMap = new Map(states.map(state => [state.id, state]))
|
|
290
316
|
|
|
@@ -318,7 +344,10 @@ export class ProjectManager {
|
|
|
318
344
|
|
|
319
345
|
let sourceHash: string | undefined
|
|
320
346
|
if (isUnitModel(library.components[instance.type])) {
|
|
321
|
-
const resolvedUnits = await this.libraryBackend.getResolvedUnitSources(
|
|
347
|
+
const resolvedUnits = await this.libraryBackend.getResolvedUnitSources(
|
|
348
|
+
projectInfo.libraryId,
|
|
349
|
+
[instance.type],
|
|
350
|
+
)
|
|
322
351
|
const resolvedUnit = resolvedUnits.find(unit => unit.unitType === instance.type)
|
|
323
352
|
|
|
324
353
|
if (resolvedUnit) {
|
|
@@ -346,28 +375,58 @@ export class ProjectManager {
|
|
|
346
375
|
inputHashInputs,
|
|
347
376
|
inputHashResolver,
|
|
348
377
|
library,
|
|
378
|
+
libraryId: projectInfo.libraryId,
|
|
349
379
|
instances: filteredInstances,
|
|
350
380
|
stateMap,
|
|
351
381
|
resolvedInputs,
|
|
352
|
-
evaluatedInputHashes,
|
|
353
382
|
}
|
|
354
383
|
}
|
|
355
384
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Ensures a library is being watched for changes.
|
|
387
|
+
* Called when a project using this library is accessed.
|
|
388
|
+
*/
|
|
389
|
+
private ensureLibraryWatched(libraryId: string): void {
|
|
390
|
+
if (this.watchedLibraries.has(libraryId)) {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.watchedLibraries.add(libraryId)
|
|
395
|
+
const abortController = new AbortController()
|
|
396
|
+
this.libraryWatchers.set(libraryId, abortController)
|
|
397
|
+
|
|
398
|
+
this.logger.debug({ libraryId }, "starting library watcher")
|
|
399
|
+
|
|
400
|
+
// Start watching this library in the background
|
|
401
|
+
void this.watchSingleLibrary(libraryId, abortController.signal)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Watches a single library for changes and handles updates.
|
|
406
|
+
*/
|
|
407
|
+
private async watchSingleLibrary(libraryId: string, signal: AbortSignal): Promise<void> {
|
|
408
|
+
try {
|
|
409
|
+
for await (const updates of this.libraryBackend.watchLibrary(libraryId, signal)) {
|
|
410
|
+
try {
|
|
411
|
+
await this.handleLibraryUpdates(libraryId, updates)
|
|
412
|
+
} catch (error) {
|
|
413
|
+
this.logger.error({ error, libraryId }, "failed to handle library updates")
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (signal.aborted) {
|
|
418
|
+
this.logger.debug({ libraryId }, "library watcher stopped")
|
|
419
|
+
} else {
|
|
420
|
+
this.logger.error({ error, libraryId }, "library watcher failed")
|
|
362
421
|
}
|
|
422
|
+
} finally {
|
|
423
|
+
this.watchedLibraries.delete(libraryId)
|
|
424
|
+
this.libraryWatchers.delete(libraryId)
|
|
363
425
|
}
|
|
364
426
|
}
|
|
365
427
|
|
|
366
|
-
private async handleLibraryUpdates(updates: LibraryUpdate[]): Promise<void> {
|
|
428
|
+
private async handleLibraryUpdates(libraryId: string, updates: LibraryUpdate[]): Promise<void> {
|
|
367
429
|
const changedComponents = new Set<string>()
|
|
368
|
-
const library = await this.libraryBackend.loadLibrary()
|
|
369
|
-
|
|
370
|
-
// TODO: handle entity updates
|
|
371
430
|
|
|
372
431
|
for (const update of updates) {
|
|
373
432
|
switch (update.type) {
|
|
@@ -377,6 +436,11 @@ export class ProjectManager {
|
|
|
377
436
|
case "component-removed":
|
|
378
437
|
changedComponents.add(update.componentType)
|
|
379
438
|
break
|
|
439
|
+
// Handle entity updates if needed
|
|
440
|
+
case "entity-updated":
|
|
441
|
+
case "entity-removed":
|
|
442
|
+
// Entity updates don't directly affect composite instance evaluation
|
|
443
|
+
break
|
|
380
444
|
}
|
|
381
445
|
}
|
|
382
446
|
|
|
@@ -385,49 +449,126 @@ export class ProjectManager {
|
|
|
385
449
|
}
|
|
386
450
|
|
|
387
451
|
this.logger.info(
|
|
388
|
-
{ changedComponents },
|
|
389
|
-
"library components changed, updating composite instances",
|
|
452
|
+
{ libraryId, changedComponents: Array.from(changedComponents) },
|
|
453
|
+
"library components changed, updating composite instances for affected projects",
|
|
390
454
|
)
|
|
391
455
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const { instances } = await this.prepareResolvers(projectId)
|
|
456
|
+
// Get all projects and find those using this libraryId
|
|
457
|
+
const allProjectIds = await this.projectBackend.getProjectIds()
|
|
395
458
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
459
|
+
for (const projectId of allProjectIds) {
|
|
460
|
+
try {
|
|
461
|
+
// Check if this project uses the changed library
|
|
462
|
+
const projectInfo = await this.projectBackend.getProjectInfo(projectId)
|
|
463
|
+
if (projectInfo.libraryId !== libraryId) {
|
|
464
|
+
continue // Skip projects that don't use this library
|
|
465
|
+
}
|
|
402
466
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
)
|
|
467
|
+
// Load the updated library for filtering
|
|
468
|
+
const library = await this.libraryBackend.loadLibrary(libraryId)
|
|
469
|
+
const { instances } = await this.prepareResolvers(projectId)
|
|
407
470
|
|
|
408
|
-
|
|
409
|
-
|
|
471
|
+
const affectedInstances = instances.filter(
|
|
472
|
+
instance =>
|
|
473
|
+
changedComponents.has(instance.type) &&
|
|
474
|
+
library.components[instance.type] &&
|
|
475
|
+
!isUnitModel(library.components[instance.type]),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if (affectedInstances.length > 0) {
|
|
479
|
+
this.logger.info(
|
|
480
|
+
{
|
|
481
|
+
projectId,
|
|
482
|
+
libraryId,
|
|
483
|
+
affectedInstanceIds: affectedInstances.map(instance => instance.id),
|
|
484
|
+
},
|
|
485
|
+
"updating composite instances for project due to library changes",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
await this.evaluateChangedCompositeInstances(projectId)
|
|
489
|
+
}
|
|
410
490
|
} catch (error) {
|
|
411
|
-
this.logger.error(
|
|
491
|
+
this.logger.error(
|
|
492
|
+
{ error, projectId, libraryId },
|
|
493
|
+
"failed to evaluate composite instances for project during library update",
|
|
494
|
+
)
|
|
412
495
|
}
|
|
413
496
|
}
|
|
414
497
|
}
|
|
415
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Cleanup method to stop all library watchers.
|
|
501
|
+
* Should be called when the ProjectManager is being shut down.
|
|
502
|
+
*/
|
|
503
|
+
dispose(): void {
|
|
504
|
+
for (const [libraryId, abortController] of this.libraryWatchers.entries()) {
|
|
505
|
+
this.logger.debug({ libraryId }, "stopping library watcher")
|
|
506
|
+
abortController.abort()
|
|
507
|
+
}
|
|
508
|
+
this.libraryWatchers.clear()
|
|
509
|
+
this.watchedLibraries.clear()
|
|
510
|
+
}
|
|
511
|
+
|
|
416
512
|
static create(
|
|
417
513
|
projectBackend: ProjectBackend,
|
|
418
|
-
stateBackend: StateBackend,
|
|
419
514
|
libraryBackend: LibraryBackend,
|
|
420
|
-
projectLockManager: ProjectLockManager,
|
|
421
515
|
stateManager: StateManager,
|
|
516
|
+
instanceLockService: InstanceLockService,
|
|
517
|
+
pubsubManager: PubSubManager,
|
|
422
518
|
logger: Logger,
|
|
423
519
|
): ProjectManager {
|
|
424
520
|
return new ProjectManager(
|
|
425
521
|
projectBackend,
|
|
426
|
-
stateBackend,
|
|
427
522
|
libraryBackend,
|
|
428
|
-
projectLockManager,
|
|
429
523
|
stateManager,
|
|
524
|
+
instanceLockService,
|
|
525
|
+
pubsubManager,
|
|
430
526
|
logger.child({ service: "ProjectManager" }),
|
|
431
527
|
)
|
|
432
528
|
}
|
|
529
|
+
|
|
530
|
+
static createSuccessEvaluationMessage(
|
|
531
|
+
result: InstanceEvaluationResult & { success: true },
|
|
532
|
+
): string {
|
|
533
|
+
const treeNodes = new Map<string, TreeNode>()
|
|
534
|
+
|
|
535
|
+
// the order of composite instances are guaranteed to be topologically sorted
|
|
536
|
+
for (const compositeInstance of result.compositeInstances) {
|
|
537
|
+
const node: TreeNode = {
|
|
538
|
+
text: compositeInstance.id,
|
|
539
|
+
children: [],
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
treeNodes.set(compositeInstance.id, node)
|
|
543
|
+
|
|
544
|
+
const parentNode = compositeInstance.instance.parentId
|
|
545
|
+
? treeNodes.get(compositeInstance.instance.parentId)
|
|
546
|
+
: undefined
|
|
547
|
+
|
|
548
|
+
if (parentNode) {
|
|
549
|
+
parentNode.children.push(node)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const child of compositeInstance.children) {
|
|
553
|
+
const childNode: TreeNode = {
|
|
554
|
+
text: child.id,
|
|
555
|
+
children: [],
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
node.children.push(childNode)
|
|
559
|
+
treeNodes.set(child.id, childNode)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// get the root node
|
|
564
|
+
const rootNode = treeNodes.get(result.instanceId)
|
|
565
|
+
if (!rootNode) {
|
|
566
|
+
return `Composite instance evaluation completed successfully, but failed to build the tree structure.`
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// render the tree structure
|
|
570
|
+
const tree = renderTree(rootNode)
|
|
571
|
+
|
|
572
|
+
return `Composite instance evaluation completed successfully.\n\nInstance Tree:\n${tree}`
|
|
573
|
+
}
|
|
433
574
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export interface PubSubBackend {
|
|
4
|
+
/**
|
|
5
|
+
* Subscribes to a topic and returns an async iterator for the messages.
|
|
6
|
+
*/
|
|
7
|
+
subscribe(topic: string, schema: z.ZodType, signal?: AbortSignal): AsyncIterable<unknown>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Publishes a message to a topic.
|
|
11
|
+
*/
|
|
12
|
+
publish(topic: string, schema: z.ZodType, message: unknown): Promise<void>
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { PubSubBackend } from "./abstractions"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
import { LocalPubSubBackend } from "./local"
|
|
5
|
+
|
|
6
|
+
export const pubSubBackendConfig = z.object({
|
|
7
|
+
HIGHSTATE_PUBSUB_BACKEND_TYPE: z.enum(["local"]).default("local"),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export function createPubSubBackend(
|
|
11
|
+
config: z.infer<typeof pubSubBackendConfig>,
|
|
12
|
+
logger: Logger,
|
|
13
|
+
): PubSubBackend {
|
|
14
|
+
switch (config.HIGHSTATE_PUBSUB_BACKEND_TYPE) {
|
|
15
|
+
case "local": {
|
|
16
|
+
return LocalPubSubBackend.create(logger)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import type { PubSubBackend } from "./abstractions"
|
|
3
|
+
import type { z } from "zod"
|
|
4
|
+
import { EventEmitter, on } from "node:events"
|
|
5
|
+
|
|
6
|
+
export class LocalPubSubBackend implements PubSubBackend {
|
|
7
|
+
private readonly eventEmitter = new EventEmitter()
|
|
8
|
+
|
|
9
|
+
private constructor(private readonly logger: Logger) {}
|
|
10
|
+
|
|
11
|
+
async *subscribe<TSchema extends z.ZodSchema>(
|
|
12
|
+
topic: string,
|
|
13
|
+
_schema: TSchema,
|
|
14
|
+
signal?: AbortSignal,
|
|
15
|
+
): AsyncIterable<z.infer<TSchema>> {
|
|
16
|
+
for await (const [event] of on(this.eventEmitter, topic, { signal })) {
|
|
17
|
+
// do not validate here because all IPC happends in the same process
|
|
18
|
+
yield event as z.infer<TSchema>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
publish<TSchema extends z.ZodSchema>(
|
|
23
|
+
topic: string,
|
|
24
|
+
_schema: TSchema,
|
|
25
|
+
message: z.infer<TSchema>,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
this.eventEmitter.emit(topic, message)
|
|
28
|
+
this.logger.trace({ topic, message }, "published message to topic")
|
|
29
|
+
|
|
30
|
+
return Promise.resolve()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static create(logger: Logger): PubSubBackend {
|
|
34
|
+
return new LocalPubSubBackend(logger)
|
|
35
|
+
}
|
|
36
|
+
}
|