@highstate/backend 0.9.15 → 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-Y7DXREVO.js +1745 -0
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +7227 -2501
- 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 +76 -185
- 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 -98
- package/dist/shared/index.js.map +1 -1
- package/package.json +31 -10
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +109 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +138 -0
- 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/api-key.ts +65 -0
- package/src/business/artifact.ts +289 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +10 -0
- package/src/business/instance-lock.ts +125 -0
- package/src/business/instance-state.ts +434 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +260 -0
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +281 -0
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +398 -0
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +5 -1
- package/src/common/performance.ts +44 -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/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +19 -11
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +100 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +24 -28
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +91 -111
- package/src/library/worker/evaluator.ts +36 -73
- package/src/library/worker/loader.lite.ts +54 -0
- package/src/library/worker/main.ts +15 -66
- package/src/library/worker/protocol.ts +6 -33
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/lock/index.ts +4 -0
- package/src/lock/manager.ts +97 -0
- package/src/lock/memory.ts +19 -0
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +118 -90
- package/src/orchestrator/operation-workset.ts +181 -93
- package/src/orchestrator/operation.ts +1021 -283
- package/src/project/abstractions.ts +27 -38
- package/src/project/evaluation.ts +248 -0
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +1 -2
- package/src/project/local.ts +107 -103
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/{workspace → pubsub}/index.ts +1 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +108 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +155 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +292 -372
- package/src/{common → runner}/pulumi.ts +89 -37
- package/src/services.ts +251 -40
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/{library.ts → models/backend/library.ts} +4 -4
- package/src/shared/models/backend/project.ts +82 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +68 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +65 -0
- package/src/shared/models/project/artifact.ts +83 -0
- package/src/shared/models/project/index.ts +13 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +98 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +449 -0
- package/src/shared/models/project/terminal.ts +98 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +38 -0
- package/src/shared/models/project/worker.ts +107 -0
- package/src/shared/resolvers/graph-resolver.ts +61 -18
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +47 -13
- package/src/shared/resolvers/registry.ts +3 -2
- package/src/shared/resolvers/state.ts +2 -2
- package/src/shared/resolvers/validation.ts +82 -25
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +4 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +199 -131
- package/src/state/encryption.ts +98 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +4 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +106 -0
- package/src/state/local/collection.ts +361 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +875 -18
- 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/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +507 -0
- package/src/state/test.ts +457 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +131 -79
- package/src/terminal/run.sh.ts +21 -11
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +167 -0
- package/dist/chunk-KTGKNSKM.js +0 -979
- package/dist/chunk-KTGKNSKM.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/project/manager.ts +0 -433
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/project.ts +0 -62
- package/src/shared/state.ts +0 -247
- package/src/shared/terminal.ts +0 -14
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- package/src/workspace/local.ts +0 -54
|
@@ -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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type TreeNode = {
|
|
2
|
+
text: string
|
|
3
|
+
children: TreeNode[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders a tree structure similar to the Linux tree command output.
|
|
8
|
+
* Uses box-drawing characters to create visual hierarchy.
|
|
9
|
+
*/
|
|
10
|
+
export function renderTree(node: TreeNode): string {
|
|
11
|
+
const lines: string[] = []
|
|
12
|
+
|
|
13
|
+
function renderNode(node: TreeNode, prefix: string = "", isLast: boolean = true): void {
|
|
14
|
+
// Add current node
|
|
15
|
+
lines.push(prefix + (isLast ? "└── " : "├── ") + node.text)
|
|
16
|
+
|
|
17
|
+
// Add children
|
|
18
|
+
const childPrefix = prefix + (isLast ? " " : "│ ")
|
|
19
|
+
node.children.forEach((child, index) => {
|
|
20
|
+
const isLastChild = index === node.children.length - 1
|
|
21
|
+
renderNode(child, childPrefix, isLastChild)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Start with root node (no prefix)
|
|
26
|
+
lines.push(node.text)
|
|
27
|
+
node.children.forEach((child, index) => {
|
|
28
|
+
const isLastChild = index === node.children.length - 1
|
|
29
|
+
renderNode(child, "", isLastChild)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return lines.join("\n")
|
|
33
|
+
}
|
package/src/common/utils.ts
CHANGED
|
@@ -34,13 +34,25 @@ export function isAbortError(error: unknown): boolean {
|
|
|
34
34
|
return error instanceof Error && error.name === "AbortError"
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const abortMessagePatterns = [
|
|
37
|
+
const abortMessagePatterns = [
|
|
38
|
+
"Operation aborted",
|
|
39
|
+
"This operation was aborted",
|
|
40
|
+
"Command was killed with SIGINT",
|
|
41
|
+
]
|
|
38
42
|
|
|
39
43
|
export function isAbortErrorLike(error: unknown): boolean {
|
|
44
|
+
if (isAbortError(error)) {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
if (error instanceof Error) {
|
|
41
49
|
return abortMessagePatterns.some(pattern => error.message.includes(pattern))
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
if (typeof error === "string") {
|
|
53
|
+
return abortMessagePatterns.some(pattern => error.includes(pattern))
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
return false
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -61,3 +73,30 @@ export function errorToString(error: unknown): string {
|
|
|
61
73
|
|
|
62
74
|
return JSON.stringify(error)
|
|
63
75
|
}
|
|
76
|
+
|
|
77
|
+
export function valueToString(value: unknown): string {
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return JSON.stringify(value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function stringToValue(value: string): unknown {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(value)
|
|
88
|
+
} catch {
|
|
89
|
+
return value
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function waitForAbort(signal: AbortSignal): Promise<void> {
|
|
94
|
+
return new Promise(resolve => {
|
|
95
|
+
if (signal.aborted) {
|
|
96
|
+
resolve()
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
signal.addEventListener("abort", () => resolve(), { once: true })
|
|
101
|
+
})
|
|
102
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
import { libraryBackendConfig } from "./library"
|
|
3
3
|
import { projectBackendConfig } from "./project"
|
|
4
|
-
import { secretBackendConfig } from "./secret"
|
|
5
4
|
import { terminalBackendConfig } from "./terminal"
|
|
6
5
|
import { runnerBackendConfig } from "./runner"
|
|
7
|
-
import { stateBackendConfig } from "./state"
|
|
8
|
-
import {
|
|
6
|
+
import { stateBackendConfig, stateManagerConfig } from "./state"
|
|
7
|
+
import { artifactBackendConfig } from "./artifact"
|
|
8
|
+
import { pubSubBackendConfig } from "./pubsub"
|
|
9
|
+
import { lockBackendConfig } from "./lock"
|
|
10
|
+
import { hotStateBackendConfig } from "./hotstate"
|
|
11
|
+
import { workerBackendConfig, workerManagerConfig } from "./worker"
|
|
9
12
|
|
|
10
13
|
const loggerConfig = z.object({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
HIGHSTATE_BACKEND_LOGGER_LEVEL: z
|
|
14
|
-
.enum(["fatal", "error", "warn", "info", "debug", "trace"])
|
|
15
|
-
.default("info"),
|
|
14
|
+
HIGHSTATE_LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"),
|
|
16
15
|
})
|
|
17
16
|
|
|
18
17
|
const configSchema = z.object({
|
|
18
|
+
...hotStateBackendConfig.shape,
|
|
19
|
+
...pubSubBackendConfig.shape,
|
|
20
|
+
...lockBackendConfig.shape,
|
|
19
21
|
...libraryBackendConfig.shape,
|
|
20
22
|
...projectBackendConfig.shape,
|
|
21
|
-
...secretBackendConfig.shape,
|
|
22
23
|
...stateBackendConfig.shape,
|
|
24
|
+
...stateManagerConfig.shape,
|
|
23
25
|
...runnerBackendConfig.shape,
|
|
24
26
|
...terminalBackendConfig.shape,
|
|
25
|
-
...
|
|
27
|
+
...workerBackendConfig.shape,
|
|
28
|
+
...workerManagerConfig.shape,
|
|
29
|
+
...artifactBackendConfig.shape,
|
|
26
30
|
...loggerConfig.shape,
|
|
27
31
|
})
|
|
28
32
|
|
|
@@ -36,5 +40,9 @@ export async function loadConfig(
|
|
|
36
40
|
await import("dotenv/config")
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
try {
|
|
44
|
+
return configSchema.parse(env)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error("Failed to parse backend configuration", { cause: error })
|
|
47
|
+
}
|
|
40
48
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export interface HotStateBackend {
|
|
4
|
+
/**
|
|
5
|
+
* Gets a value for a key.
|
|
6
|
+
*/
|
|
7
|
+
get(key: string, schema: z.ZodType): Promise<unknown>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a key exists.
|
|
11
|
+
*/
|
|
12
|
+
exists(key: string): Promise<boolean>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sets a value for a key.
|
|
16
|
+
*/
|
|
17
|
+
set(key: string, schema: z.ZodType, value: unknown): Promise<void>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deletes a key.
|
|
21
|
+
*/
|
|
22
|
+
delete(key: string): Promise<void>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets all key-value pairs from a hash set.
|
|
26
|
+
*/
|
|
27
|
+
hgetall(key: string, schema: z.ZodType): Promise<Array<[string, unknown]>>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a field exists in a hash set.
|
|
31
|
+
*/
|
|
32
|
+
hexists(key: string, field: string): Promise<boolean>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sets a field in a hash set.
|
|
36
|
+
*/
|
|
37
|
+
hset(key: string, field: string, schema: z.ZodType, value: unknown): Promise<void>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets a field from a hash set.
|
|
41
|
+
*/
|
|
42
|
+
hget(key: string, field: string, schema: z.ZodType): Promise<unknown>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deletes a field from a hash set.
|
|
46
|
+
*/
|
|
47
|
+
hdel(key: string, field: string): Promise<void>
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HotStateBackend } from "./abstractions"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { MemoryHotStateBackend } from "./memory"
|
|
4
|
+
|
|
5
|
+
export const hotStateBackendConfig = z.object({
|
|
6
|
+
HIGHSTATE_HOTSTATE_BACKEND_TYPE: z.enum(["memory"]).default("memory"),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export function createHotStateBackend(
|
|
10
|
+
config: z.infer<typeof hotStateBackendConfig>,
|
|
11
|
+
): HotStateBackend {
|
|
12
|
+
switch (config.HIGHSTATE_HOTSTATE_BACKEND_TYPE) {
|
|
13
|
+
case "memory": {
|
|
14
|
+
return MemoryHotStateBackend.create()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|