@highstate/backend 0.7.2 → 0.7.4

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} +1254 -915
  2. package/dist/library/source-resolution-worker.js +55 -0
  3. package/dist/library/worker/main.js +216 -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 +15 -19
  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 +110 -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,404 @@
1
+ import type {
2
+ InstanceEvaluationResult,
3
+ LibraryBackend,
4
+ ModuleEvaluationResult,
5
+ ResolvedUnitSource,
6
+ } from "./abstractions"
7
+ import type { Logger } from "pino"
8
+ import type { HighstateManifestJson, SourceResolutionResult } from "./source-resolution-worker"
9
+ import type { WorkerRequest, WorkerResponse } from "./worker/protocol"
10
+ import { fileURLToPath } from "node:url"
11
+ import { EventEmitter, on } from "node:events"
12
+ import { Worker } from "node:worker_threads"
13
+ import { basename, dirname, relative, resolve } from "node:path"
14
+ import { readFile } from "node:fs/promises"
15
+ import { type InstanceModel, isUnitModel, type UnitModel } from "@highstate/contract"
16
+ import Watcher from "watcher"
17
+ import { BetterLock } from "better-lock"
18
+ import { resolve as importMetaResolve } from "import-meta-resolve"
19
+ import { z } from "zod"
20
+ import { readPackageJSON, resolvePackageJSON } from "pkg-types"
21
+ import { resolveMainLocalProject, stringArrayType } from "../common"
22
+ import {
23
+ diffLibraries,
24
+ type LibraryModel,
25
+ type LibraryUpdate,
26
+ type ResolvedInstanceInput,
27
+ } from "../shared"
28
+
29
+ export const localLibraryBackendConfig = z.object({
30
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES: stringArrayType.default("@highstate/library"),
31
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_SOURCE_BASE_PATH: z.string().optional(),
32
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_EXTRA_SOURCE_WATCH_PATHS: stringArrayType.default(""),
33
+ })
34
+
35
+ interface Events {
36
+ library: [LibraryUpdate[]]
37
+ resolvedUnitSource: [ResolvedUnitSource]
38
+ }
39
+
40
+ export class LocalLibraryBackend implements LibraryBackend {
41
+ private readonly libraryWatcher: Watcher
42
+ private readonly sourceWatcher: Watcher
43
+
44
+ private readonly lock = new BetterLock()
45
+ private readonly eventEmitter = new EventEmitter<Events>()
46
+
47
+ private library: LibraryModel | null = null
48
+ private worker: Worker | null = null
49
+
50
+ private readonly resolvedUnitSources = new Map<string, ResolvedUnitSource>()
51
+
52
+ private constructor(
53
+ private readonly modulePaths: string[],
54
+ private readonly sourceBasePath: string,
55
+ extraSourceWatchPaths: string[],
56
+ private readonly logger: Logger,
57
+ ) {
58
+ this.libraryWatcher = new Watcher(modulePaths, { recursive: true, ignoreInitial: true })
59
+ this.libraryWatcher.on("all", (event: string, path: string) => {
60
+ const prefixPath = modulePaths.find(modulePath => path.startsWith(modulePath))
61
+
62
+ this.logger.info({ msg: "library event", event, path: relative(prefixPath!, path) })
63
+ void this.lock.acquire(() => this.updateLibrary())
64
+ })
65
+
66
+ this.sourceWatcher = new Watcher([sourceBasePath, ...extraSourceWatchPaths], {
67
+ recursive: true,
68
+ ignoreInitial: true,
69
+ ignore: /\.git|node_modules/,
70
+ })
71
+
72
+ this.sourceWatcher.on("all", (_, path: string) => {
73
+ if (!path.endsWith("highstate.manifest.json")) {
74
+ return
75
+ }
76
+
77
+ void this.updateUnitSourceHashes(path)
78
+ })
79
+
80
+ this.logger.debug({ msg: "initialized", modulePaths })
81
+ }
82
+
83
+ async loadLibrary(): Promise<LibraryModel> {
84
+ return await this.lock.acquire(async () => {
85
+ const [library] = await this.getLibrary()
86
+
87
+ return library
88
+ })
89
+ }
90
+
91
+ async *watchLibrary(signal?: AbortSignal): AsyncIterable<LibraryUpdate[]> {
92
+ for await (const [library] of on(this.eventEmitter, "library", { signal })) {
93
+ yield library
94
+ }
95
+ }
96
+
97
+ async getResolvedUnitSources(): Promise<readonly ResolvedUnitSource[]> {
98
+ return await this.lock.acquire(async () => {
99
+ // ensure library is loaded
100
+ const [library] = await this.getLibrary()
101
+
102
+ if (!this.resolvedUnitSources.size) {
103
+ await this.syncUnitSources(library)
104
+ }
105
+
106
+ return Array.from(this.resolvedUnitSources.values())
107
+ })
108
+ }
109
+
110
+ async getResolvedUnitSource(unitType: string): Promise<ResolvedUnitSource | null> {
111
+ return await this.lock.acquire(async () => {
112
+ // ensure library is loaded
113
+ const [library] = await this.getLibrary()
114
+
115
+ if (!this.resolvedUnitSources.size) {
116
+ await this.syncUnitSources(library)
117
+ }
118
+
119
+ return this.resolvedUnitSources.get(unitType) ?? null
120
+ })
121
+ }
122
+
123
+ async *watchResolvedUnitSources(signal?: AbortSignal): AsyncIterable<ResolvedUnitSource> {
124
+ for await (const [resolvedUnitSource] of on(this.eventEmitter, "resolvedUnitSource", {
125
+ signal,
126
+ })) {
127
+ yield resolvedUnitSource
128
+ }
129
+ }
130
+
131
+ async syncUnitSources(library: LibraryModel): Promise<void> {
132
+ const unitsToResolve = new Map<string, UnitModel>()
133
+
134
+ for (const component of Object.values(library.components)) {
135
+ if (!isUnitModel(component)) {
136
+ continue
137
+ }
138
+
139
+ const existingResolvedSource = this.resolvedUnitSources.get(component.type)
140
+ const expectedSource = JSON.stringify(component.source)
141
+
142
+ if (existingResolvedSource?.serializedSource !== expectedSource) {
143
+ unitsToResolve.set(component.type, component)
144
+ }
145
+ }
146
+
147
+ await this.runSourceResolution(unitsToResolve)
148
+ }
149
+
150
+ private async runSourceResolution(units: Map<string, UnitModel>): Promise<void> {
151
+ const workerPathUrl = importMetaResolve(
152
+ `@highstate/backend/source-resolution-worker`,
153
+ import.meta.url,
154
+ )
155
+ const workerPath = fileURLToPath(workerPathUrl)
156
+
157
+ const worker = new Worker(workerPath, {
158
+ workerData: {
159
+ requests: Array.from(units.values()).map(unit => ({
160
+ unitType: unit.type,
161
+ source: unit.source,
162
+ })),
163
+ sourceBasePath: this.sourceBasePath,
164
+ logLevel: "error",
165
+ },
166
+ })
167
+
168
+ for await (const [event] of on(worker, "message")) {
169
+ const eventData = event as {
170
+ type: string
171
+ results: readonly SourceResolutionResult[]
172
+ }
173
+
174
+ if (eventData.type !== "result") {
175
+ throw new Error(`Unexpected message type '${eventData.type}', expected 'result'`)
176
+ }
177
+
178
+ for (const result of eventData.results) {
179
+ const unit = units.get(result.unitType)
180
+ if (!unit) {
181
+ this.logger.warn("unit not found for resolved source", { unitType: result.unitType })
182
+ continue
183
+ }
184
+
185
+ const resolvedSource: ResolvedUnitSource = {
186
+ unitType: result.unitType,
187
+ serializedSource: JSON.stringify(unit.source),
188
+ projectPath: result.projectPath,
189
+ packageJsonPath: result.packageJsonPath,
190
+ allowedDependencies: result.allowedDependencies,
191
+ sourceHash: result.sourceHash,
192
+ }
193
+
194
+ this.resolvedUnitSources.set(result.unitType, resolvedSource)
195
+ this.eventEmitter.emit("resolvedUnitSource", resolvedSource)
196
+ }
197
+
198
+ this.logger.info("unit sources synced")
199
+ return
200
+ }
201
+
202
+ throw new Error("Worker ended without sending the result")
203
+ }
204
+
205
+ async evaluateCompositeInstances(
206
+ allInstances: InstanceModel[],
207
+ resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,
208
+ instanceIds: string[],
209
+ ): Promise<InstanceEvaluationResult[]> {
210
+ return await this.lock.acquire(async () => {
211
+ this.logger.info("evaluating %d composite instances", instanceIds.length)
212
+
213
+ const [, worker] = await this.getLibrary()
214
+ worker.postMessage({
215
+ type: "evaluate-composite-instances",
216
+ allInstances,
217
+ resolvedInputs,
218
+ instanceIds,
219
+ } satisfies WorkerRequest)
220
+
221
+ this.logger.debug("evaluation request sent")
222
+
223
+ const { results } = await this.getResult(worker, "instance-evaluation-results")
224
+ return results
225
+ })
226
+ }
227
+
228
+ async evaluateModules(modulePaths: string[]): Promise<ModuleEvaluationResult> {
229
+ return await this.lock.acquire(async () => {
230
+ this.logger.info({ msg: "evaluating modules", modulePaths })
231
+
232
+ const [, worker] = await this.getLibrary()
233
+ worker.postMessage({
234
+ type: "evaluate-modules",
235
+ modulePaths,
236
+ } satisfies WorkerRequest)
237
+
238
+ this.logger.debug("evaluation request sent")
239
+
240
+ const { result } = await this.getResult(worker, "module-evaluation-result")
241
+ return result
242
+ })
243
+ }
244
+
245
+ private async getResult<TType extends WorkerResponse["type"]>(
246
+ worker: Worker,
247
+ expectedType: TType,
248
+ ): Promise<WorkerResponse & { type: TType }> {
249
+ for await (const [event] of on(worker, "message")) {
250
+ const eventData = event as WorkerResponse
251
+
252
+ if (eventData.type === "error") {
253
+ throw new Error(`Worker error: ${eventData.error}`)
254
+ }
255
+
256
+ if (eventData.type !== expectedType) {
257
+ throw new Error(
258
+ `Unexpected response message type "${eventData.type}", expected "${expectedType}"`,
259
+ )
260
+ }
261
+
262
+ return eventData as WorkerResponse & { type: TType }
263
+ }
264
+
265
+ throw new Error("Worker ended without sending any response")
266
+ }
267
+
268
+ private async getLibrary(): Promise<[LibraryModel, Worker]> {
269
+ if (this.library && this.worker) {
270
+ return [this.library, this.worker]
271
+ }
272
+
273
+ return await this.updateLibrary()
274
+ }
275
+
276
+ private async updateLibrary(): Promise<[LibraryModel, Worker]> {
277
+ this.logger.info("creating library worker")
278
+ this.worker = this.createWorker({ modulePaths: this.modulePaths, logLevel: "silent" })
279
+
280
+ for await (const [event] of on(this.worker, "message")) {
281
+ const eventData = event as {
282
+ type: string
283
+ library: LibraryModel
284
+ error?: string
285
+ }
286
+
287
+ if (eventData.type === "error") {
288
+ throw new Error(`Worker error: ${eventData.error}`)
289
+ }
290
+
291
+ if (eventData.type !== "library") {
292
+ throw new Error(`Unexpected message type '${eventData.type}', expected 'library'`)
293
+ }
294
+
295
+ const updates = diffLibraries(
296
+ this.library ?? { components: {}, entities: {} },
297
+ eventData.library,
298
+ )
299
+
300
+ this.eventEmitter.emit("library", updates)
301
+ this.library = eventData.library
302
+
303
+ this.logger.info("library reloaded")
304
+
305
+ await this.syncUnitSources(eventData.library)
306
+
307
+ return [this.library, this.worker]
308
+ }
309
+
310
+ throw new Error("Worker ended without sending library model")
311
+ }
312
+
313
+ private createWorker(workerData: Record<string, unknown>) {
314
+ const workerPathUrl = importMetaResolve(`@highstate/backend/library-worker`, import.meta.url)
315
+ const workerPath = fileURLToPath(workerPathUrl)
316
+
317
+ return new Worker(workerPath, { workerData })
318
+ }
319
+
320
+ private async updateUnitSourceHashes(path: string): Promise<void> {
321
+ const packageJsonPath = await resolvePackageJSON(path)
322
+ const packageJson = await readPackageJSON(path)
323
+ const library = await this.loadLibrary()
324
+
325
+ const manifestPath = resolve(dirname(packageJsonPath), "dist", "highstate.manifest.json")
326
+
327
+ let manifest: HighstateManifestJson | undefined
328
+ try {
329
+ manifest = JSON.parse(await readFile(manifestPath, "utf8")) as HighstateManifestJson
330
+ } catch (error) {
331
+ this.logger.debug(
332
+ { error },
333
+ `failed to read highstate manifest for package "%s"`,
334
+ packageJson.name,
335
+ )
336
+ }
337
+
338
+ for (const unit of Object.values(library.components)) {
339
+ if (!isUnitModel(unit)) {
340
+ continue
341
+ }
342
+
343
+ if (unit.source.package !== packageJson.name) {
344
+ continue
345
+ }
346
+
347
+ // TODO: resolve the path
348
+ const relativePath = unit.source.path
349
+ ? `./dist/${unit.source.path}/index.js`
350
+ : `./dist/index.js`
351
+
352
+ const sourceHash = manifest?.sourceHashes?.[relativePath]
353
+
354
+ if (!sourceHash) {
355
+ this.logger.warn(`source hash not found for unit: "%s"`, unit.type)
356
+ continue
357
+ }
358
+
359
+ const resolvedSource = this.resolvedUnitSources.get(unit.type)
360
+ if (!resolvedSource) {
361
+ this.logger.warn(`resolved source not found for unit: "%s"`, unit.type)
362
+ continue
363
+ }
364
+
365
+ const newResolvedSource: ResolvedUnitSource = {
366
+ ...resolvedSource,
367
+ sourceHash,
368
+ }
369
+
370
+ this.resolvedUnitSources.set(unit.type, newResolvedSource)
371
+ this.eventEmitter.emit("resolvedUnitSource", newResolvedSource)
372
+ this.logger.info(`updated source hash for unit: "%s"`, unit.type)
373
+ }
374
+ }
375
+
376
+ static async create(config: z.infer<typeof localLibraryBackendConfig>, logger: Logger) {
377
+ const modulePaths: string[] = []
378
+
379
+ for (const module of config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES) {
380
+ const url = importMetaResolve(module, import.meta.url)
381
+ let path = fileURLToPath(url)
382
+ if (basename(path).includes(".")) {
383
+ path = dirname(path)
384
+ }
385
+
386
+ modulePaths.push(path)
387
+ }
388
+
389
+ let sourceBasePath = config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_SOURCE_BASE_PATH
390
+ const extraSourceWatchPaths = config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_EXTRA_SOURCE_WATCH_PATHS
391
+ if (!sourceBasePath) {
392
+ const [projectPath] = await resolveMainLocalProject()
393
+ sourceBasePath = resolve(projectPath, "units")
394
+ extraSourceWatchPaths.push(projectPath)
395
+ }
396
+
397
+ return new LocalLibraryBackend(
398
+ modulePaths,
399
+ sourceBasePath,
400
+ extraSourceWatchPaths,
401
+ logger.child({ backend: "LibraryBackend", service: "LocalLibraryBackend" }),
402
+ )
403
+ }
404
+ }
@@ -0,0 +1,96 @@
1
+ import type { UnitSource } from "@highstate/contract"
2
+ import { parentPort, workerData } from "node:worker_threads"
3
+ import { fileURLToPath } from "node:url"
4
+ import { dirname, relative, resolve } from "node:path"
5
+ import { readFile } from "node:fs/promises"
6
+ import pino, { type Level } from "pino"
7
+ import { resolve as importMetaResolve } from "import-meta-resolve"
8
+ import { readPackageJSON, resolvePackageJSON } from "pkg-types"
9
+
10
+ export type SourceResolutionRequest = {
11
+ unitType: string
12
+ source: UnitSource
13
+ }
14
+
15
+ export type SourceResolutionResult = {
16
+ unitType: string
17
+ projectPath: string
18
+ packageJsonPath: string
19
+ sourceHash: string
20
+ allowedDependencies: string[]
21
+ }
22
+
23
+ export type HighstateManifestJson = {
24
+ sourceHashes?: Record<string, string>
25
+ }
26
+
27
+ const { requests, logLevel } = workerData as {
28
+ requests: SourceResolutionRequest[]
29
+ logLevel?: Level
30
+ }
31
+
32
+ const logger = pino({ name: "source-resolution-worker", level: logLevel ?? "silent" })
33
+
34
+ const results = await Promise.all(
35
+ requests.map(request => resolveUnitSourceSafe(request.source, request.unitType)),
36
+ )
37
+
38
+ parentPort!.postMessage({
39
+ type: "result",
40
+ results: results.filter((result): result is SourceResolutionResult => result !== null),
41
+ })
42
+
43
+ async function resolveUnitSourceSafe(
44
+ source: UnitSource,
45
+ unitType: string,
46
+ ): Promise<SourceResolutionResult | null> {
47
+ try {
48
+ return await resolveUnitSource(source, unitType)
49
+ } catch (error) {
50
+ logger.error({ source, unitType, err: error }, "failed to resolve unit source")
51
+ return null
52
+ }
53
+ }
54
+
55
+ async function resolveUnitSource(
56
+ source: UnitSource,
57
+ unitType: string,
58
+ ): Promise<SourceResolutionResult> {
59
+ const fullPath = source.path ? `${source.package}/${source.path}` : source.package
60
+
61
+ const url = importMetaResolve(fullPath, import.meta.url)
62
+ const path = fileURLToPath(url)
63
+ const projectPath = dirname(path)
64
+
65
+ const packageJsonPath = await resolvePackageJSON(projectPath)
66
+ const packageJson = await readPackageJSON(projectPath)
67
+
68
+ const manifestPath = resolve(dirname(packageJsonPath), "dist", "highstate.manifest.json")
69
+ let manifest: HighstateManifestJson | undefined
70
+ try {
71
+ manifest = JSON.parse(await readFile(manifestPath, "utf8")) as HighstateManifestJson
72
+ } catch (error) {
73
+ logger.debug({ error }, "failed to read highstate manifest")
74
+ }
75
+
76
+ let relativePath = relative(dirname(packageJsonPath), path)
77
+ relativePath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`
78
+
79
+ const sourceHash = manifest?.sourceHashes?.[relativePath]
80
+ if (!sourceHash) {
81
+ logger.warn({ unitType, relativePath, packageName: packageJson.name }, "source hash not found")
82
+ }
83
+
84
+ // only the peer dependencies of the package are allowed to auto-install when they are missing
85
+ const allowedDependencies = Object.keys(packageJson.peerDependencies ?? {})
86
+
87
+ logger.debug({ packageJson }, "package.json read")
88
+
89
+ return {
90
+ unitType,
91
+ projectPath,
92
+ packageJsonPath,
93
+ sourceHash: sourceHash ?? "",
94
+ allowedDependencies,
95
+ }
96
+ }
@@ -0,0 +1,119 @@
1
+ import type { Logger } from "pino"
2
+ import type { Library } from "./loader"
3
+ import type { Jiti } from "jiti"
4
+ import type { InstanceEvaluationResult, ModuleEvaluationResult } from "../abstractions"
5
+ import type { ResolvedInstanceInput } from "../../shared"
6
+ import { getCompositeInstances, resetEvaluation, type InstanceModel } from "@highstate/contract"
7
+ import { BetterLock } from "better-lock"
8
+ import { errorToString } from "../../common"
9
+
10
+ const lock = new BetterLock()
11
+
12
+ export function evaluateModules(
13
+ jiti: Jiti,
14
+ logger: Logger,
15
+ modulePaths: string[],
16
+ ): Promise<ModuleEvaluationResult> {
17
+ return lock.acquire(async () => {
18
+ resetEvaluation()
19
+ let lastModulePath = ""
20
+
21
+ try {
22
+ for (const modulePath of modulePaths) {
23
+ logger.info("loading module: %s", modulePath)
24
+
25
+ lastModulePath = modulePath
26
+ await jiti.import(modulePath)
27
+
28
+ logger.debug("module loaded: %s", modulePath)
29
+ }
30
+
31
+ return {
32
+ success: true,
33
+ compositeInstances: getCompositeInstances(),
34
+ }
35
+ } catch (error) {
36
+ return {
37
+ success: false,
38
+ modulePath: lastModulePath,
39
+ error: errorToString(error),
40
+ }
41
+ }
42
+ })
43
+ }
44
+
45
+ export function evaluateInstances(
46
+ logger: Logger,
47
+ library: Library,
48
+ allInstances: InstanceModel[],
49
+ resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,
50
+ instanceIds: string[],
51
+ ): InstanceEvaluationResult[] {
52
+ const results: InstanceEvaluationResult[] = []
53
+ const allInstancesMap = new Map(allInstances.map(instance => [instance.id, instance]))
54
+ const instanceOutputs = new Map<string, Record<string, unknown>>()
55
+
56
+ for (const instanceId of instanceIds ?? []) {
57
+ try {
58
+ logger.debug({ instanceId }, "evaluating top-level instance")
59
+ resetEvaluation()
60
+
61
+ evaluateInstance(instanceId)
62
+
63
+ results.push({
64
+ success: true,
65
+ instanceId,
66
+ compositeInstances: getCompositeInstances(),
67
+ })
68
+ } catch (error) {
69
+ results.push({
70
+ success: false,
71
+ instanceId,
72
+ error: errorToString(error),
73
+ })
74
+ }
75
+ }
76
+
77
+ return results
78
+
79
+ function evaluateInstance(instanceId: string): Record<string, unknown> {
80
+ let outputs = instanceOutputs.get(instanceId)
81
+
82
+ if (!outputs) {
83
+ outputs = _evaluateInstance(instanceId)
84
+ instanceOutputs.set(instanceId, outputs)
85
+ }
86
+
87
+ return outputs
88
+ }
89
+
90
+ function _evaluateInstance(instanceId: string): Record<string, unknown> {
91
+ const inputs: Record<string, unknown> = {}
92
+ const instance = allInstancesMap.get(instanceId)
93
+
94
+ logger.info("evaluating instance", { instanceId })
95
+
96
+ if (!instance) {
97
+ throw new Error(`Instance not found: ${instanceId}`)
98
+ }
99
+
100
+ for (const [inputName, input] of Object.entries(resolvedInputs[instanceId] ?? {})) {
101
+ inputs[inputName] = input.map(input => {
102
+ const evaluated = evaluateInstance(input.input.instanceId)
103
+
104
+ return evaluated[input.input.output]
105
+ })
106
+ }
107
+
108
+ const component = library.components[instance.type]
109
+ if (!component) {
110
+ throw new Error(`Component not found: ${instance.type}, required by instance: ${instanceId}`)
111
+ }
112
+
113
+ return component({
114
+ name: instance.name,
115
+ args: instance.args as Record<string, never>,
116
+ inputs: inputs as Record<string, never>,
117
+ })
118
+ }
119
+ }