@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.
- package/dist/chunk-NAAIDR4U.js +8499 -0
- package/dist/chunk-NAAIDR4U.js.map +1 -0
- package/dist/chunk-OU5OQBLB.js +74 -0
- package/dist/chunk-OU5OQBLB.js.map +1 -0
- package/dist/{chunk-WHALQHEZ.js → chunk-Y7DXREVO.js} +502 -774
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +2979 -2233
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +7 -5
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +40 -41
- package/dist/library/worker/main.js.map +1 -1
- package/dist/magic-string.es-5ABAC4JN.js +1292 -0
- package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
- package/dist/shared/index.js +3 -216
- package/dist/shared/index.js.map +1 -1
- package/package.json +9 -6
- package/src/artifact/encryption.ts +47 -7
- package/src/artifact/factory.ts +2 -2
- package/src/artifact/local.ts +2 -6
- package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
- package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
- package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
- package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
- package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
- package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
- package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
- package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
- package/src/business/artifact.ts +2 -1
- package/src/business/index.ts +1 -0
- package/src/business/instance-lock.ts +3 -2
- package/src/business/instance-state.ts +202 -60
- package/src/business/project-unlock.ts +41 -23
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +139 -45
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +289 -52
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +3 -0
- package/src/common/random.ts +68 -0
- package/src/common/test/index.ts +2 -0
- package/src/common/test/render.ts +98 -0
- package/src/common/test/tracer.ts +359 -0
- package/src/config.ts +5 -1
- package/src/hotstate/manager.ts +8 -8
- package/src/hotstate/validation.ts +0 -1
- package/src/library/abstractions.ts +20 -11
- package/src/library/local.ts +6 -13
- package/src/library/worker/evaluator.ts +30 -34
- package/src/library/worker/loader.lite.ts +13 -0
- package/src/library/worker/main.ts +8 -8
- package/src/library/worker/protocol.ts +0 -11
- package/src/lock/index.ts +1 -0
- package/src/lock/manager.ts +17 -2
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +17 -36
- package/src/orchestrator/operation-workset.ts +34 -37
- package/src/orchestrator/operation.ts +129 -74
- package/src/project/abstractions.ts +27 -51
- package/src/project/evaluation.ts +248 -0
- package/src/project/index.ts +1 -1
- package/src/project/local.ts +75 -127
- package/src/pubsub/manager.ts +21 -13
- package/src/runner/abstractions.ts +29 -9
- package/src/runner/artifact-env.ts +3 -3
- package/src/runner/local.ts +29 -19
- package/src/runner/pulumi.ts +4 -1
- package/src/services.ts +77 -24
- package/src/shared/models/backend/library.ts +4 -4
- package/src/shared/models/backend/project.ts +25 -6
- package/src/shared/models/backend/unlock-method.ts +1 -1
- package/src/shared/models/base.ts +1 -84
- package/src/shared/models/project/api-key.ts +5 -2
- package/src/shared/models/project/artifact.ts +3 -33
- package/src/shared/models/project/index.ts +1 -2
- package/src/shared/models/project/lock.ts +3 -3
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/models/project/operation.ts +3 -3
- package/src/shared/models/project/page.ts +3 -3
- package/src/shared/models/project/secret.ts +4 -18
- package/src/shared/models/project/service-account.ts +2 -2
- package/src/shared/models/project/state.ts +32 -15
- package/src/shared/models/project/terminal.ts +4 -5
- package/src/shared/models/project/trigger.ts +1 -1
- package/src/shared/models/project/unlock-method.ts +9 -2
- package/src/shared/models/project/worker.ts +9 -7
- package/src/shared/resolvers/graph-resolver.ts +41 -26
- package/src/shared/resolvers/input.ts +47 -5
- package/src/shared/resolvers/validation.ts +23 -7
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/utils/index.ts +1 -0
- package/src/state/abstractions.ts +98 -259
- package/src/state/encryption.ts +39 -0
- package/src/state/index.ts +1 -0
- package/src/state/local/backend.ts +29 -222
- package/src/state/local/collection.ts +105 -86
- package/src/state/manager.ts +358 -287
- package/src/state/memory/backend.ts +70 -0
- package/src/state/memory/collection.ts +270 -0
- package/src/state/memory/index.ts +2 -0
- package/src/state/repository/repository.index.ts +1 -1
- package/src/state/repository/repository.ts +71 -22
- package/src/state/test.ts +457 -0
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/manager.ts +28 -0
- package/dist/chunk-RCB4AFGD.js +0 -159
- package/dist/chunk-RCB4AFGD.js.map +0 -1
- package/dist/chunk-WHALQHEZ.js.map +0 -1
- package/src/project/manager.ts +0 -574
- package/src/shared/models/project/component.ts +0 -45
- 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
|
-
|
|
43
|
+
try {
|
|
44
|
+
return configSchema.parse(env)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error("Failed to parse backend configuration", { cause: error })
|
|
47
|
+
}
|
|
44
48
|
}
|
package/src/hotstate/manager.ts
CHANGED
|
@@ -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.
|
|
9
|
+
data: z.ZodBoolean
|
|
14
10
|
}
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
const dataSchemas: Record<string, z.ZodType> = {
|
|
18
|
-
"
|
|
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
|
|
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
|
|
11
|
+
export type ProjectEvaluationResult =
|
|
12
12
|
| {
|
|
13
13
|
success: true
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
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<
|
|
73
|
+
): Promise<ProjectEvaluationResult>
|
|
65
74
|
}
|
package/src/library/local.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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<
|
|
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
|
-
|
|
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 {
|
|
2
|
+
import type { ProjectEvaluationResult } from "../abstractions"
|
|
3
3
|
import type { ResolvedInstanceInput } from "../../shared"
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
):
|
|
19
|
-
const
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
}
|