@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.
- package/dist/{index.mjs → index.js} +1254 -915
- package/dist/library/source-resolution-worker.js +55 -0
- package/dist/library/worker/main.js +216 -0
- package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
- package/dist/shared/index.js +6 -0
- package/dist/utils-ByadNcv4.js +102 -0
- package/package.json +15 -19
- package/src/common/index.ts +3 -0
- package/src/common/local.ts +22 -0
- package/src/common/pulumi.ts +230 -0
- package/src/common/utils.ts +137 -0
- package/src/config.ts +40 -0
- package/src/index.ts +6 -0
- package/src/library/abstractions.ts +83 -0
- package/src/library/factory.ts +20 -0
- package/src/library/index.ts +2 -0
- package/src/library/local.ts +404 -0
- package/src/library/source-resolution-worker.ts +96 -0
- package/src/library/worker/evaluator.ts +119 -0
- package/src/library/worker/loader.ts +110 -0
- package/src/library/worker/main.ts +82 -0
- package/src/library/worker/protocol.ts +38 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/manager.ts +165 -0
- package/src/orchestrator/operation-workset.ts +483 -0
- package/src/orchestrator/operation.ts +647 -0
- package/src/preferences/shared.ts +1 -0
- package/src/project/abstractions.ts +89 -0
- package/src/project/factory.ts +11 -0
- package/src/project/index.ts +4 -0
- package/src/project/local.ts +412 -0
- package/src/project/lock.ts +39 -0
- package/src/project/manager.ts +374 -0
- package/src/runner/abstractions.ts +146 -0
- package/src/runner/factory.ts +22 -0
- package/src/runner/index.ts +2 -0
- package/src/runner/local.ts +698 -0
- package/src/secret/abstractions.ts +59 -0
- package/src/secret/factory.ts +22 -0
- package/src/secret/index.ts +2 -0
- package/src/secret/local.ts +152 -0
- package/src/services.ts +133 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/library.ts +77 -0
- package/src/shared/operation.ts +85 -0
- package/src/shared/project.ts +62 -0
- package/src/shared/resolvers/graph-resolver.ts +111 -0
- package/src/shared/resolvers/input-hash.ts +77 -0
- package/src/shared/resolvers/input.ts +314 -0
- package/src/shared/resolvers/registry.ts +10 -0
- package/src/shared/resolvers/validation.ts +94 -0
- package/src/shared/state.ts +262 -0
- package/src/shared/terminal.ts +13 -0
- package/src/state/abstractions.ts +222 -0
- package/src/state/factory.ts +22 -0
- package/src/state/index.ts +3 -0
- package/src/state/local.ts +605 -0
- package/src/state/manager.ts +33 -0
- package/src/terminal/docker.ts +90 -0
- package/src/terminal/factory.ts +20 -0
- package/src/terminal/index.ts +3 -0
- package/src/terminal/manager.ts +330 -0
- package/src/terminal/run.sh.ts +37 -0
- package/src/terminal/shared.ts +50 -0
- package/src/workspace/abstractions.ts +41 -0
- package/src/workspace/factory.ts +14 -0
- package/src/workspace/index.ts +2 -0
- package/src/workspace/local.ts +54 -0
- package/dist/index.d.ts +0 -760
- package/dist/library/worker/main.mjs +0 -164
- package/dist/runner/source-resolution-worker.mjs +0 -22
- package/dist/shared/index.d.ts +0 -85
- package/dist/shared/index.mjs +0 -54
- 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
|
+
}
|