@highstate/backend 0.9.16 → 0.9.18

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 (125) hide show
  1. package/dist/chunk-NAAIDR4U.js +8499 -0
  2. package/dist/chunk-NAAIDR4U.js.map +1 -0
  3. package/dist/chunk-OU5OQBLB.js +74 -0
  4. package/dist/chunk-OU5OQBLB.js.map +1 -0
  5. package/dist/{chunk-WHALQHEZ.js → chunk-Y7DXREVO.js} +502 -774
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +2979 -2233
  9. package/dist/index.js.map +1 -1
  10. package/dist/library/package-resolution-worker.js +7 -5
  11. package/dist/library/package-resolution-worker.js.map +1 -1
  12. package/dist/library/worker/main.js +40 -41
  13. package/dist/library/worker/main.js.map +1 -1
  14. package/dist/magic-string.es-5ABAC4JN.js +1292 -0
  15. package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
  16. package/dist/shared/index.js +3 -216
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +9 -6
  19. package/src/artifact/encryption.ts +47 -7
  20. package/src/artifact/factory.ts +2 -2
  21. package/src/artifact/local.ts +2 -6
  22. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  23. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  27. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  28. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  40. package/src/business/artifact.ts +2 -1
  41. package/src/business/index.ts +1 -0
  42. package/src/business/instance-lock.ts +3 -2
  43. package/src/business/instance-state.ts +202 -60
  44. package/src/business/project-unlock.ts +41 -23
  45. package/src/business/project.ts +299 -0
  46. package/src/business/secret.test.ts +178 -0
  47. package/src/business/secret.ts +139 -45
  48. package/src/business/worker.test.ts +614 -0
  49. package/src/business/worker.ts +289 -52
  50. package/src/common/clock.ts +18 -0
  51. package/src/common/index.ts +3 -0
  52. package/src/common/random.ts +68 -0
  53. package/src/common/test/index.ts +2 -0
  54. package/src/common/test/render.ts +98 -0
  55. package/src/common/test/tracer.ts +359 -0
  56. package/src/config.ts +5 -1
  57. package/src/hotstate/manager.ts +8 -8
  58. package/src/hotstate/validation.ts +0 -1
  59. package/src/library/abstractions.ts +20 -11
  60. package/src/library/local.ts +6 -13
  61. package/src/library/worker/evaluator.ts +30 -34
  62. package/src/library/worker/loader.lite.ts +13 -0
  63. package/src/library/worker/main.ts +8 -8
  64. package/src/library/worker/protocol.ts +0 -11
  65. package/src/lock/index.ts +1 -0
  66. package/src/lock/manager.ts +17 -2
  67. package/src/lock/test.ts +108 -0
  68. package/src/orchestrator/manager.ts +17 -36
  69. package/src/orchestrator/operation-workset.ts +34 -37
  70. package/src/orchestrator/operation.ts +129 -74
  71. package/src/project/abstractions.ts +27 -51
  72. package/src/project/evaluation.ts +248 -0
  73. package/src/project/index.ts +1 -1
  74. package/src/project/local.ts +75 -127
  75. package/src/pubsub/manager.ts +21 -13
  76. package/src/runner/abstractions.ts +29 -9
  77. package/src/runner/artifact-env.ts +3 -3
  78. package/src/runner/local.ts +29 -19
  79. package/src/runner/pulumi.ts +4 -1
  80. package/src/services.ts +77 -24
  81. package/src/shared/models/backend/library.ts +4 -4
  82. package/src/shared/models/backend/project.ts +25 -6
  83. package/src/shared/models/backend/unlock-method.ts +1 -1
  84. package/src/shared/models/base.ts +1 -84
  85. package/src/shared/models/project/api-key.ts +5 -2
  86. package/src/shared/models/project/artifact.ts +3 -33
  87. package/src/shared/models/project/index.ts +1 -2
  88. package/src/shared/models/project/lock.ts +3 -3
  89. package/src/shared/models/project/model.ts +14 -0
  90. package/src/shared/models/project/operation.ts +3 -3
  91. package/src/shared/models/project/page.ts +3 -3
  92. package/src/shared/models/project/secret.ts +4 -18
  93. package/src/shared/models/project/service-account.ts +2 -2
  94. package/src/shared/models/project/state.ts +32 -15
  95. package/src/shared/models/project/terminal.ts +4 -5
  96. package/src/shared/models/project/trigger.ts +1 -1
  97. package/src/shared/models/project/unlock-method.ts +9 -2
  98. package/src/shared/models/project/worker.ts +9 -7
  99. package/src/shared/resolvers/graph-resolver.ts +41 -26
  100. package/src/shared/resolvers/input.ts +47 -5
  101. package/src/shared/resolvers/validation.ts +23 -7
  102. package/src/shared/utils/args.ts +25 -0
  103. package/src/shared/utils/index.ts +1 -0
  104. package/src/state/abstractions.ts +98 -259
  105. package/src/state/encryption.ts +39 -0
  106. package/src/state/index.ts +1 -0
  107. package/src/state/local/backend.ts +29 -222
  108. package/src/state/local/collection.ts +105 -86
  109. package/src/state/manager.ts +358 -287
  110. package/src/state/memory/backend.ts +70 -0
  111. package/src/state/memory/collection.ts +270 -0
  112. package/src/state/memory/index.ts +2 -0
  113. package/src/state/repository/repository.index.ts +1 -1
  114. package/src/state/repository/repository.ts +71 -22
  115. package/src/state/test.ts +457 -0
  116. package/src/unlock/abstractions.ts +49 -0
  117. package/src/unlock/index.ts +2 -0
  118. package/src/unlock/memory.ts +32 -0
  119. package/src/worker/manager.ts +28 -0
  120. package/dist/chunk-RCB4AFGD.js +0 -159
  121. package/dist/chunk-RCB4AFGD.js.map +0 -1
  122. package/dist/chunk-WHALQHEZ.js.map +0 -1
  123. package/src/project/manager.ts +0 -574
  124. package/src/shared/models/project/component.ts +0 -45
  125. package/src/shared/models/project/instance.ts +0 -74
@@ -0,0 +1,359 @@
1
+ import { PassThrough } from "stream"
2
+ import { basename } from "node:path"
3
+ import pino, { levels } from "pino"
4
+ import { test as baseTest, type RunnerTask } from "vitest"
5
+ import { isPromise, omit } from "remeda"
6
+ import * as md from "ts-markdown-builder"
7
+ import { ReproducibleClockProvider, type ClockProvider } from "../clock"
8
+ import { ReproducibleRandomProvider, type RandomProvider } from "../random"
9
+ import { renderTraceEntry } from "./render"
10
+
11
+ export interface TraceEntry {
12
+ readonly id: number
13
+ title?: string
14
+ render(): string
15
+ }
16
+
17
+ export function linkTraceEntry(entry: TraceEntry): string {
18
+ const anchorId = `#trace-${entry.id}`
19
+
20
+ if (entry.title) {
21
+ return md.link(anchorId, `${entry.id}. ${entry.title}`)
22
+ }
23
+
24
+ return md.link(anchorId, entry.id.toString())
25
+ }
26
+
27
+ export type TestPhase = "arrange" | "act" | "assert"
28
+
29
+ class CallTraceEntry implements TraceEntry {
30
+ resolvedEntry?: TraceEntry
31
+ result?: unknown
32
+ error?: unknown
33
+
34
+ constructor(
35
+ readonly id: number,
36
+ private readonly serviceName: string,
37
+ private readonly methodName: string,
38
+ private readonly args: unknown[],
39
+ result?: unknown,
40
+ error?: unknown,
41
+ ) {
42
+ this.result = result
43
+ this.error = error
44
+ }
45
+
46
+ get title(): string {
47
+ return `${this.serviceName}.${this.methodName}`
48
+ }
49
+
50
+ render(): string {
51
+ if (this.resolvedEntry) {
52
+ // deferred completion - show promise link with ➡️
53
+ return renderTraceEntry({
54
+ icon: "➡️",
55
+ title: this.title,
56
+ fields: {
57
+ args: {
58
+ value: this.args,
59
+ alwaysRender: true,
60
+ },
61
+ result: {
62
+ value: `${md.code("Promise")}, resolved at ${linkTraceEntry(this.resolvedEntry)}`,
63
+ raw: true,
64
+ },
65
+ },
66
+ })
67
+ }
68
+
69
+ return renderTraceEntry({
70
+ icon: "✅",
71
+ title: this.title,
72
+ fields: {
73
+ args: {
74
+ value: this.args,
75
+ alwaysRender: true,
76
+ },
77
+ result: {
78
+ value: this.result,
79
+ },
80
+ error: {
81
+ value: this.error,
82
+ },
83
+ },
84
+ })
85
+ }
86
+ }
87
+
88
+ class CallResultEntry implements TraceEntry {
89
+ constructor(
90
+ readonly id: number,
91
+ private readonly callEntry: TraceEntry,
92
+ private readonly result?: unknown,
93
+ private readonly error?: unknown,
94
+ ) {}
95
+
96
+ render(): string {
97
+ return renderTraceEntry({
98
+ icon: "↩️",
99
+ title: linkTraceEntry(this.callEntry),
100
+ fields: {
101
+ result: {
102
+ value: this.result,
103
+ },
104
+ error: {
105
+ value: this.error,
106
+ },
107
+ },
108
+ })
109
+ }
110
+ }
111
+
112
+ const logLevelIcons: Record<string, string> = {
113
+ error: "❌",
114
+ warn: "⚠️",
115
+ info: "ℹ️",
116
+ debug: "🔍",
117
+ trace: "🔬",
118
+ }
119
+
120
+ class LogTraceEntry implements TraceEntry {
121
+ constructor(
122
+ readonly id: number,
123
+ private readonly level: string,
124
+ private readonly message: string,
125
+ private readonly data?: Record<string, unknown>,
126
+ private readonly error?: unknown,
127
+ ) {}
128
+
129
+ render(): string {
130
+ return renderTraceEntry({
131
+ icon: logLevelIcons[this.level] ?? "ℹ️",
132
+ title: this.level,
133
+ code: this.message,
134
+ fields: {
135
+ data: {
136
+ value: this.data,
137
+ },
138
+ error: {
139
+ value: this.error,
140
+ },
141
+ },
142
+ })
143
+ }
144
+ }
145
+
146
+ export class TestTracer {
147
+ private readonly traces: Record<TestPhase, TraceEntry[]> = {
148
+ arrange: [],
149
+ act: [],
150
+ assert: [],
151
+ }
152
+
153
+ private _entryCount = 0
154
+ private _currentPhase: TestPhase = "arrange"
155
+
156
+ setPhase(phase: TestPhase) {
157
+ this._currentPhase = phase
158
+ }
159
+
160
+ readonly logger = pino({ level: "debug" }, this.createLogStream())
161
+
162
+ createServiceMock<TService extends object>(name: string, impl: Partial<TService> = {}): TService {
163
+ return new Proxy(impl as TService, {
164
+ get: (target, prop) => {
165
+ // we assume that all get calls via the proxy are method calls
166
+
167
+ return this.createServiceMethodMock(
168
+ name,
169
+ prop as string,
170
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
171
+ target[prop as keyof TService] as Function,
172
+ )
173
+ },
174
+ })
175
+ }
176
+
177
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
178
+ private createServiceMethodMock(serviceName: string, name: string, base: Function = () => {}) {
179
+ return new Proxy(base, {
180
+ apply: (target, thisArg, args) => {
181
+ try {
182
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
183
+ const result = target.apply(thisArg, args)
184
+
185
+ if (!isPromise(result)) {
186
+ // record synchronous call
187
+ this.addEntry(new CallTraceEntry(this.nextEntryId(), serviceName, name, args, result))
188
+
189
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
190
+ return result
191
+ }
192
+
193
+ // create async call trace entry
194
+ const callEntry = new CallTraceEntry(this.nextEntryId(), serviceName, name, args)
195
+ this.addEntry(callEntry)
196
+
197
+ // wrap the promise to track resolution
198
+ const wrappedPromise = (result as Promise<unknown>).then(
199
+ (resolvedValue: unknown) => {
200
+ if (this.nextEntryId() === callEntry.id + 1) {
201
+ // immediate completion (no other traces added in between)
202
+
203
+ callEntry.result = resolvedValue
204
+ return resolvedValue
205
+ }
206
+
207
+ // deferred completion - create a separate result entry
208
+ const resultEntry = new CallResultEntry(this.nextEntryId(), callEntry, resolvedValue)
209
+
210
+ this.addEntry(resultEntry)
211
+ callEntry.resolvedEntry = resultEntry
212
+
213
+ return resolvedValue
214
+ },
215
+ (rejectedError: unknown) => {
216
+ if (this.nextEntryId() === callEntry.id + 1) {
217
+ // immediate error (no other traces added in between)
218
+
219
+ callEntry.error = rejectedError
220
+ throw rejectedError
221
+ }
222
+
223
+ // deferred error - create a separate result entry
224
+ const resultEntry = new CallResultEntry(
225
+ this.nextEntryId(),
226
+ callEntry,
227
+ undefined,
228
+ rejectedError,
229
+ )
230
+
231
+ this.addEntry(resultEntry)
232
+ callEntry.resolvedEntry = resultEntry
233
+
234
+ throw rejectedError
235
+ },
236
+ )
237
+
238
+ return wrappedPromise
239
+ } catch (error) {
240
+ // record synchronous error
241
+ const callEntry = new CallTraceEntry(
242
+ this.nextEntryId(),
243
+ serviceName,
244
+ name,
245
+ args,
246
+ undefined,
247
+ error,
248
+ )
249
+
250
+ this.addEntry(callEntry)
251
+ }
252
+ },
253
+ })
254
+ }
255
+
256
+ nextEntryId(): number {
257
+ return this._entryCount + 1
258
+ }
259
+
260
+ addEntry(entry: TraceEntry) {
261
+ this._entryCount += 1
262
+ this.traces[this._currentPhase].push(entry)
263
+ }
264
+
265
+ private createLogStream() {
266
+ const stream = new PassThrough()
267
+
268
+ stream.on("data", data => {
269
+ const { level, msg, error, ...other } = JSON.parse(String(data)) as {
270
+ msg: string
271
+ level: number
272
+ error?: string
273
+ }
274
+
275
+ const levelLabel = levels.labels[level]
276
+ const logEntry = new LogTraceEntry(
277
+ this.nextEntryId(),
278
+ levelLabel,
279
+ msg,
280
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
281
+ omit(other as any, ["time", "pid", "hostname"]),
282
+ error,
283
+ )
284
+
285
+ this.addEntry(logEntry)
286
+ })
287
+
288
+ return stream
289
+ }
290
+
291
+ render(): string {
292
+ const blocks = Object.entries(this.traces).flatMap(([phase, traces]) => {
293
+ if (traces.length === 0) {
294
+ return []
295
+ }
296
+
297
+ return [
298
+ `## ${phase}\n`,
299
+ ...traces.map(trace => {
300
+ const anchorId = `trace-${trace.id}`
301
+
302
+ return `### <a id="${anchorId}"></a> ${trace.id}. ${trace.render()}`
303
+ }),
304
+ ]
305
+ })
306
+
307
+ return md.joinBlocks(blocks)
308
+ }
309
+ }
310
+
311
+ export type TestBaseFixtures = {
312
+ tracer: TestTracer
313
+ clock: ClockProvider
314
+ random: RandomProvider
315
+ }
316
+
317
+ function camelCaseToKebabCase(str: string): string {
318
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
319
+ }
320
+
321
+ function getFullName(test: RunnerTask): string {
322
+ let result = camelCaseToKebabCase(test.name)
323
+
324
+ while (test.suite) {
325
+ test = test.suite
326
+ result = `${camelCaseToKebabCase(test.name)}/${result}`
327
+ }
328
+
329
+ return result.replaceAll(" ", "-")
330
+ }
331
+
332
+ export const test = baseTest.extend<TestBaseFixtures>({
333
+ tracer: async ({ task, expect }, use) => {
334
+ const tracer = new TestTracer()
335
+
336
+ await use(tracer)
337
+
338
+ const fileName = basename(task.file.name).replace(/\.test\.ts$/, "")
339
+ const taskName = getFullName(task)
340
+ const rendered = tracer.render()
341
+
342
+ await expect(rendered).toMatchFileSnapshot(`./__traces__/${fileName}/${taskName}.md`)
343
+ },
344
+
345
+ random: async ({ task }, use) => {
346
+ const random = new ReproducibleRandomProvider(Buffer.from(task.id))
347
+
348
+ await use(random)
349
+ },
350
+
351
+ // eslint-disable-next-line no-empty-pattern
352
+ clock: async ({}, use) => {
353
+ const clock = new ReproducibleClockProvider()
354
+
355
+ await use(clock)
356
+ },
357
+ })
358
+
359
+ export const testBase = test
package/src/config.ts CHANGED
@@ -40,5 +40,9 @@ export async function loadConfig(
40
40
  await import("dotenv/config")
41
41
  }
42
42
 
43
- return configSchema.parse(env)
43
+ try {
44
+ return configSchema.parse(env)
45
+ } catch (error) {
46
+ throw new Error("Failed to parse backend configuration", { cause: error })
47
+ }
44
48
  }
@@ -1,21 +1,17 @@
1
1
  import type { HotStateBackend } from "./abstractions"
2
2
  import { z } from "zod"
3
3
  import { instanceLockSchema, instanceStateSchema, operationSchema } from "../shared"
4
+ import { MemoryHotStateBackend } from "./memory"
4
5
 
5
6
  export type DataMap = {
6
- /**
7
- * Master key for a project used to read and write encrypted data.
8
- *
9
- * If the key is not set, the project is considered locked.
10
- */
11
- "project-master-key": {
7
+ "instance-states-loaded": {
12
8
  key: [projectId: string]
13
- data: z.ZodType<Uint8Array>
9
+ data: z.ZodBoolean
14
10
  }
15
11
  }
16
12
 
17
13
  const dataSchemas: Record<string, z.ZodType> = {
18
- "project-master-key": z.instanceof(Uint8Array),
14
+ "instance-states-loaded": z.boolean(),
19
15
  }
20
16
 
21
17
  export type DataHashSetMap = {
@@ -190,3 +186,7 @@ export class HotStateManager {
190
186
  return await this.hotStateBackend.hdel(key.join(":"), field)
191
187
  }
192
188
  }
189
+
190
+ export function createTestHotStateManager(): HotStateManager {
191
+ return new HotStateManager(new MemoryHotStateBackend())
192
+ }
@@ -47,7 +47,6 @@ export class HotStateValidationDecorator implements HotStateBackend {
47
47
 
48
48
  for (const [field, value] of entries) {
49
49
  try {
50
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
51
50
  const validValue = schema.parse(value)
52
51
  validEntries.push([field, validValue])
53
52
  } catch (error) {
@@ -1,4 +1,4 @@
1
- import type { InstanceModel, CompositeInstance } from "@highstate/contract"
1
+ import type { InstanceModel } from "@highstate/contract"
2
2
  import type { LibraryModel, LibraryUpdate, ResolvedInstanceInput } from "../shared"
3
3
 
4
4
  export type ResolvedUnitSource = {
@@ -8,15 +8,18 @@ export type ResolvedUnitSource = {
8
8
  allowedDependencies: string[]
9
9
  }
10
10
 
11
- export type InstanceEvaluationResult =
11
+ export type ProjectEvaluationResult =
12
12
  | {
13
13
  success: true
14
- instanceId: string
15
- compositeInstances: CompositeInstance[]
14
+ virtualInstances: InstanceModel[]
15
+
16
+ /**
17
+ * The mapping of top-level composite instance IDs to error messages if any.
18
+ */
19
+ topLevelErrors: Record<string, string>
16
20
  }
17
21
  | {
18
22
  success: false
19
- instanceId: string
20
23
  error: string
21
24
  }
22
25
 
@@ -24,27 +27,33 @@ export interface LibraryBackend {
24
27
  /**
25
28
  * Loads the library.
26
29
  */
27
- loadLibrary(libraryId: string, signal?: AbortSignal): Promise<LibraryModel>
30
+ loadLibrary(libraryId: string | undefined, signal?: AbortSignal): Promise<LibraryModel>
28
31
 
29
32
  /**
30
33
  * Watches the library for changes.
31
34
  */
32
- watchLibrary(libraryId: string, signal?: AbortSignal): AsyncIterable<LibraryUpdate[]>
35
+ watchLibrary(libraryId: string | undefined, signal?: AbortSignal): AsyncIterable<LibraryUpdate[]>
33
36
 
34
37
  /**
35
38
  * Gets the resolved unit sources for the given unit types.
36
39
  *
37
40
  * If the packages for these units are not resolved, it will resolve them and include in watch list.
38
41
  */
39
- getResolvedUnitSources(libraryId: string, unitTypes: string[]): Promise<ResolvedUnitSource[]>
42
+ getResolvedUnitSources(
43
+ libraryId: string | undefined,
44
+ unitTypes: string[],
45
+ ): Promise<ResolvedUnitSource[]>
40
46
 
41
47
  /**
42
48
  * Watches the resolved unit sources for changes.
43
49
  * Returns an async iterable that emits each resolved unit source whenever it changes.
44
50
  * Does not emit the resolved unit sources for units that have not changed even if the library was reloaded.
51
+ *
52
+ * @param libraryId The library ID to watch for resolved unit sources.
53
+ * @param signal Optional AbortSignal to cancel the watch.
45
54
  */
46
55
  watchResolvedUnitSources(
47
- libraryId: string,
56
+ libraryId: string | undefined,
48
57
  signal?: AbortSignal,
49
58
  ): AsyncIterable<ResolvedUnitSource>
50
59
 
@@ -57,9 +66,9 @@ export interface LibraryBackend {
57
66
  * @param instanceIds The instance ids to evaluate.
58
67
  */
59
68
  evaluateCompositeInstances(
60
- libraryId: string,
69
+ libraryId: string | undefined,
61
70
  allInstances: InstanceModel[],
62
71
  resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,
63
72
  instanceIds: string[],
64
- ): Promise<InstanceEvaluationResult[]>
73
+ ): Promise<ProjectEvaluationResult>
65
74
  }
@@ -1,10 +1,10 @@
1
- import type { InstanceEvaluationResult, LibraryBackend, ResolvedUnitSource } from "./abstractions"
1
+ import type { LibraryBackend, ProjectEvaluationResult, ResolvedUnitSource } from "./abstractions"
2
2
  import type { Logger } from "pino"
3
3
  import type {
4
4
  PackageResolutionResponse,
5
5
  PackageResolutionWorkerData,
6
6
  } from "./package-resolution-worker"
7
- import type { WorkerData, WorkerResponse } from "./worker/protocol"
7
+ import type { WorkerData } from "./worker/protocol"
8
8
  import { fileURLToPath } from "node:url"
9
9
  import { EventEmitter, on } from "node:events"
10
10
  import { Worker } from "node:worker_threads"
@@ -28,7 +28,7 @@ import {
28
28
  } from "../shared"
29
29
 
30
30
  export const localLibraryBackendConfig = z.object({
31
- HIGHSTATE_LIBRARY_BACKEND_LOCAL_PACKAGES: stringArrayType.default("@highstate/library"),
31
+ HIGHSTATE_LIBRARY_BACKEND_LOCAL_PACKAGES: stringArrayType.default(() => ["@highstate/library"]),
32
32
  HIGHSTATE_LIBRARY_BACKEND_LOCAL_WATCH_PATHS: stringArrayType.optional(),
33
33
  })
34
34
 
@@ -149,7 +149,7 @@ export class LocalLibraryBackend implements LibraryBackend {
149
149
  allInstances: InstanceModel[],
150
150
  resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,
151
151
  instanceIds: string[],
152
- ): Promise<InstanceEvaluationResult[]> {
152
+ ): Promise<ProjectEvaluationResult> {
153
153
  this.logger.info("evaluating %d composite instances", instanceIds.length)
154
154
 
155
155
  const worker = this.createLibraryWorker({
@@ -159,15 +159,8 @@ export class LocalLibraryBackend implements LibraryBackend {
159
159
  instanceIds,
160
160
  })
161
161
 
162
- for await (const [event] of on(worker, "message")) {
163
- const eventData = event as WorkerResponse
164
-
165
- if (eventData.type === "error") {
166
- throw new Error(`Worker error: ${eventData.error}`)
167
- }
168
-
169
- this.logger.info("composite instances evaluated successfully")
170
- return eventData.results
162
+ for await (const [event] of on(worker, "message", { signal: AbortSignal.timeout(10_000) })) {
163
+ return event as ProjectEvaluationResult
171
164
  }
172
165
 
173
166
  throw new Error("Worker ended without sending any response")
@@ -1,72 +1,68 @@
1
1
  import type { Logger } from "pino"
2
- import type { InstanceEvaluationResult } from "../abstractions"
2
+ import type { ProjectEvaluationResult } from "../abstractions"
3
3
  import type { ResolvedInstanceInput } from "../../shared"
4
4
  import {
5
- getCompositeInstances,
6
- resetEvaluation,
5
+ getRuntimeInstances,
6
+ InstanceNameConflictError,
7
7
  type Component,
8
8
  type InstanceModel,
9
9
  } from "@highstate/contract"
10
10
  import { errorToString } from "../../common"
11
11
 
12
- export function evaluateInstances(
12
+ export function evaluateProject(
13
13
  logger: Logger,
14
14
  components: Readonly<Record<string, Component>>,
15
15
  allInstances: InstanceModel[],
16
16
  resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,
17
17
  instanceIds: string[],
18
- ): InstanceEvaluationResult[] {
19
- const results: InstanceEvaluationResult[] = []
18
+ ): ProjectEvaluationResult {
19
+ const errors: Record<string, string> = {}
20
20
  const allInstancesMap = new Map(allInstances.map(instance => [instance.id, instance]))
21
+ const instanceOutputs = new Map<string, Record<string, unknown>>()
21
22
 
22
23
  for (const instanceId of instanceIds ?? []) {
23
24
  try {
24
25
  logger.debug({ instanceId }, "evaluating top-level instance")
25
- resetEvaluation()
26
26
 
27
- evaluateInstance(instanceId)
28
-
29
- results.push({
30
- success: true,
31
- instanceId,
32
- compositeInstances: getCompositeInstances().filter(
33
- instance =>
34
- instanceId.includes(instance.instance.id) || !allInstancesMap.has(instance.instance.id),
35
- ),
36
- })
27
+ evaluateInstance(instanceId as InstanceModel["id"])
37
28
  } catch (error) {
38
- results.push({
39
- success: false,
40
- instanceId,
41
- error: errorToString(error),
42
- })
29
+ errors[instanceId] = errorToString(error)
30
+
31
+ if (error instanceof InstanceNameConflictError) {
32
+ // fail the whole evaluation if there's a name conflict
33
+ return {
34
+ success: false,
35
+ error: error.message,
36
+ }
37
+ }
43
38
  }
44
39
  }
45
40
 
46
- return results
41
+ return {
42
+ success: true,
43
+ virtualInstances: getRuntimeInstances()
44
+ .map(instance => instance.instance)
45
+ // only include top-level composite instances and their children
46
+ .filter(instance => instanceIds.includes(instance.id) || !allInstancesMap.has(instance.id)),
47
+ topLevelErrors: errors,
48
+ }
47
49
 
48
- function evaluateInstance(
49
- instanceId: string,
50
- instanceOutputs: Map<string, Record<string, unknown>> = new Map(),
51
- ): Record<string, unknown> {
50
+ function evaluateInstance(instanceId: InstanceModel["id"]): Record<string, unknown> {
52
51
  let outputs = instanceOutputs.get(instanceId)
53
52
 
54
53
  if (!outputs) {
55
- outputs = _evaluateInstance(instanceId, instanceOutputs)
54
+ outputs = _evaluateInstance(instanceId)
56
55
  instanceOutputs.set(instanceId, outputs)
57
56
  }
58
57
 
59
58
  return outputs
60
59
  }
61
60
 
62
- function _evaluateInstance(
63
- instanceId: string,
64
- instanceOutputs: Map<string, Record<string, unknown>>,
65
- ): Record<string, unknown> {
61
+ function _evaluateInstance(instanceId: InstanceModel["id"]): Record<string, unknown> {
66
62
  const inputs: Record<string, unknown> = {}
67
63
  const instance = allInstancesMap.get(instanceId)
68
64
 
69
- logger.info("evaluating instance", { instanceId })
65
+ logger.debug("evaluating instance", { instanceId })
70
66
 
71
67
  if (!instance) {
72
68
  throw new Error(`Instance not found: ${instanceId}`)
@@ -74,7 +70,7 @@ export function evaluateInstances(
74
70
 
75
71
  for (const [inputName, input] of Object.entries(resolvedInputs[instanceId] ?? {})) {
76
72
  inputs[inputName] = input.map(input => {
77
- const evaluated = evaluateInstance(input.input.instanceId, instanceOutputs)
73
+ const evaluated = evaluateInstance(input.input.instanceId)
78
74
 
79
75
  return evaluated[input.input.output]
80
76
  })
@@ -35,6 +35,19 @@ async function _loadLibrary(value: unknown, components: Record<string, Component
35
35
  return
36
36
  }
37
37
 
38
+ if ("_zod" in value) {
39
+ // this is a zod schema, we can skip it
40
+ return
41
+ }
42
+
43
+ if (Array.isArray(value)) {
44
+ for (const item of value) {
45
+ await _loadLibrary(item, components)
46
+ }
47
+
48
+ return
49
+ }
50
+
38
51
  for (const key in value) {
39
52
  await _loadLibrary((value as Record<string, unknown>)[key], components)
40
53
  }