@highstate/backend 0.18.0 → 0.20.0
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-JT4KWE3B.js → chunk-52MY2TCE.js} +348 -19
- package/dist/chunk-52MY2TCE.js.map +1 -0
- package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
- package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +4159 -785
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +5 -2
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +2 -2
- package/package.json +7 -7
- package/prisma/backend/_schema/object.prisma +12 -0
- package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
- package/prisma/project/artifact.prisma +3 -0
- package/prisma/project/entity.prisma +125 -0
- package/prisma/project/instance.prisma +6 -0
- package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
- package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
- package/prisma/project/operation.prisma +3 -0
- package/src/business/artifact.test.ts +22 -2
- package/src/business/artifact.ts +7 -1
- package/src/business/entity-snapshot.test.ts +684 -0
- package/src/business/entity-snapshot.ts +904 -0
- package/src/business/evaluation.test.ts +56 -0
- package/src/business/evaluation.ts +102 -22
- package/src/business/global-search.test.ts +344 -0
- package/src/business/global-search.ts +902 -0
- package/src/business/index.ts +4 -0
- package/src/business/instance-lock.ts +58 -74
- package/src/business/instance-state.test.ts +15 -1
- package/src/business/instance-state.ts +37 -14
- package/src/business/object-ref-index.test.ts +140 -0
- package/src/business/object-ref-index.ts +193 -0
- package/src/business/operation.test.ts +15 -1
- package/src/business/operation.ts +4 -0
- package/src/business/project-model.ts +154 -13
- package/src/business/project-unlock.ts +25 -2
- package/src/business/project.ts +9 -0
- package/src/business/secret.test.ts +35 -2
- package/src/business/secret.ts +32 -9
- package/src/business/settings.ts +761 -0
- package/src/business/unit-output.test.ts +477 -0
- package/src/business/unit-output.ts +461 -0
- package/src/business/worker.ts +55 -4
- package/src/database/_generated/backend/postgresql/browser.ts +6 -0
- package/src/database/_generated/backend/postgresql/client.ts +6 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
- package/src/database/_generated/backend/postgresql/models.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +6 -0
- package/src/database/_generated/backend/sqlite/client.ts +6 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
- package/src/database/_generated/backend/sqlite/models.ts +1 -0
- package/src/database/_generated/project/browser.ts +23 -0
- package/src/database/_generated/project/client.ts +23 -0
- package/src/database/_generated/project/commonInputTypes.ts +87 -53
- package/src/database/_generated/project/enums.ts +8 -0
- package/src/database/_generated/project/internal/class.ts +53 -5
- package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
- package/src/database/_generated/project/models/Artifact.ts +199 -11
- package/src/database/_generated/project/models/Entity.ts +1274 -0
- package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
- package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
- package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
- package/src/database/_generated/project/models/InstanceState.ts +361 -1
- package/src/database/_generated/project/models/Operation.ts +148 -3
- package/src/database/_generated/project/models/OperationLog.ts +0 -4
- package/src/database/_generated/project/models.ts +4 -0
- package/src/database/migration.ts +3 -0
- package/src/library/worker/evaluator.ts +7 -1
- package/src/orchestrator/manager.ts +7 -0
- package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
- package/src/orchestrator/operation-context.ts +154 -16
- package/src/orchestrator/operation-plan.destroy.test.md +33 -12
- package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
- package/src/orchestrator/operation-plan.fixtures.ts +2 -0
- package/src/orchestrator/operation-plan.md +4 -1
- package/src/orchestrator/operation-plan.ts +286 -92
- package/src/orchestrator/operation-plan.update.test.md +286 -11
- package/src/orchestrator/operation-plan.update.test.ts +656 -5
- package/src/orchestrator/operation-workset.ts +72 -22
- package/src/orchestrator/operation.cancel.test.ts +4 -0
- package/src/orchestrator/operation.composite.test.ts +341 -0
- package/src/orchestrator/operation.destroy.test.ts +4 -0
- package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
- package/src/orchestrator/operation.preview.test.ts +4 -0
- package/src/orchestrator/operation.refresh.test.ts +4 -0
- package/src/orchestrator/operation.test-utils.ts +52 -13
- package/src/orchestrator/operation.ts +228 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +110 -0
- package/src/orchestrator/operation.update.test.ts +4 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/orchestrator/unit-input-values.test.ts +450 -0
- package/src/orchestrator/unit-input-values.ts +281 -0
- package/src/pubsub/manager.ts +3 -0
- package/src/runner/abstractions.ts +23 -54
- package/src/runner/local.ts +109 -85
- package/src/services.ts +52 -1
- package/src/shared/models/prisma.ts +1 -0
- package/src/shared/models/project/entity.ts +121 -0
- package/src/shared/models/project/index.ts +1 -0
- package/src/shared/models/project/operation.ts +61 -3
- package/src/shared/models/project/state.ts +10 -0
- package/src/shared/models/project/worker.ts +7 -0
- package/src/shared/resolvers/effective-output-type.test.ts +494 -0
- package/src/shared/resolvers/effective-output-type.ts +162 -0
- package/src/shared/resolvers/index.ts +1 -0
- package/src/shared/resolvers/input.ts +61 -9
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/stable-json.ts +41 -0
- package/src/terminal/manager.ts +6 -0
- package/src/worker/manager.ts +97 -1
- package/dist/chunk-JT4KWE3B.js.map +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ComponentModel, EntityModel, InstanceInput, InstanceModel } from "@highstate/contract"
|
|
2
|
+
|
|
3
|
+
export type InstanceTypeContext = {
|
|
4
|
+
instance: InstanceModel
|
|
5
|
+
component: ComponentModel
|
|
6
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ResolveEffectiveOutputTypeOptions = {
|
|
10
|
+
input: InstanceInput
|
|
11
|
+
fallbackType: string
|
|
12
|
+
getInstanceContext: (instanceId: string) => InstanceTypeContext | undefined
|
|
13
|
+
visited?: Set<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the effective entity type of a connected output.
|
|
18
|
+
*
|
|
19
|
+
* The function starts from the connected output declaration and then applies
|
|
20
|
+
* two additional transformations used by the platform:
|
|
21
|
+
*
|
|
22
|
+
* 1. `fromInput` forwarding.
|
|
23
|
+
* If the output references `fromInput`, it follows that input edge and reuses
|
|
24
|
+
* the upstream effective type when the forwarding source is deterministic.
|
|
25
|
+
*
|
|
26
|
+
* 2. Input `path` traversal.
|
|
27
|
+
* If the edge provides a `path`, the type is narrowed by following entity
|
|
28
|
+
* inclusions segment-by-segment.
|
|
29
|
+
*
|
|
30
|
+
* The function is intentionally conservative.
|
|
31
|
+
* If a forwarding source is ambiguous (missing, multiple, hub/injection-backed,
|
|
32
|
+
* unknown node/entity, or cyclic), it falls back to the declared type.
|
|
33
|
+
*
|
|
34
|
+
* @param options.input The connected instance output reference being consumed.
|
|
35
|
+
* @param options.fallbackType The type to use when resolution cannot continue safely.
|
|
36
|
+
* @param options.getInstanceContext A lookup that returns instance/component/entities for an instance ID.
|
|
37
|
+
* @param options.visited A recursion guard set used to prevent forwarding cycles.
|
|
38
|
+
* @returns The effective resolved output entity type.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveEffectiveOutputType(options: ResolveEffectiveOutputTypeOptions): string {
|
|
41
|
+
const visited = options.visited ?? new Set<string>()
|
|
42
|
+
|
|
43
|
+
const producer = options.getInstanceContext(options.input.instanceId)
|
|
44
|
+
if (!producer) {
|
|
45
|
+
return resolveTypeByPathOrFallbackInclusion({
|
|
46
|
+
rootType: options.fallbackType,
|
|
47
|
+
path: options.input.path,
|
|
48
|
+
entities: undefined,
|
|
49
|
+
fallbackType: options.fallbackType,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const outputSpec = producer.component.outputs[options.input.output]
|
|
54
|
+
if (!outputSpec) {
|
|
55
|
+
return resolveTypeByPathOrFallbackInclusion({
|
|
56
|
+
rootType: options.fallbackType,
|
|
57
|
+
path: options.input.path,
|
|
58
|
+
entities: producer.entities,
|
|
59
|
+
fallbackType: options.fallbackType,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let effectiveType: string = outputSpec.type
|
|
64
|
+
|
|
65
|
+
if (outputSpec.fromInput) {
|
|
66
|
+
const visitedKey = `${producer.instance.id}:${options.input.output}`
|
|
67
|
+
if (!visited.has(visitedKey)) {
|
|
68
|
+
visited.add(visitedKey)
|
|
69
|
+
|
|
70
|
+
const forwardedInputName = outputSpec.fromInput
|
|
71
|
+
const forwardedFallback = producer.component.inputs[forwardedInputName]?.type ?? effectiveType
|
|
72
|
+
const hasHubInputs = (producer.instance.hubInputs?.[forwardedInputName]?.length ?? 0) > 0
|
|
73
|
+
const hasInjectionInputs = (producer.instance.injectionInputs?.length ?? 0) > 0
|
|
74
|
+
const directInputs = producer.instance.inputs?.[forwardedInputName] ?? []
|
|
75
|
+
|
|
76
|
+
if (!hasHubInputs && !hasInjectionInputs && directInputs.length === 1) {
|
|
77
|
+
const forwardedInput = directInputs[0]
|
|
78
|
+
if (forwardedInput) {
|
|
79
|
+
effectiveType = resolveEffectiveOutputType({
|
|
80
|
+
input: forwardedInput,
|
|
81
|
+
fallbackType: forwardedFallback,
|
|
82
|
+
getInstanceContext: options.getInstanceContext,
|
|
83
|
+
visited,
|
|
84
|
+
})
|
|
85
|
+
} else {
|
|
86
|
+
effectiveType = forwardedFallback
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
effectiveType = forwardedFallback
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return resolveTypeByPathOrFallbackInclusion({
|
|
95
|
+
rootType: effectiveType,
|
|
96
|
+
path: options.input.path,
|
|
97
|
+
entities: producer.entities,
|
|
98
|
+
fallbackType: options.fallbackType,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveTypeByPathOrFallbackInclusion(options: {
|
|
103
|
+
rootType: string
|
|
104
|
+
path: string | undefined
|
|
105
|
+
entities: Readonly<Record<string, EntityModel>> | undefined
|
|
106
|
+
fallbackType: string
|
|
107
|
+
}): string {
|
|
108
|
+
const { rootType, path, entities, fallbackType } = options
|
|
109
|
+
|
|
110
|
+
if (!path || !entities) {
|
|
111
|
+
return resolveTypeByImplicitInclusion(rootType, fallbackType, entities)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let currentType = rootType
|
|
115
|
+
const segments = path.split(".")
|
|
116
|
+
|
|
117
|
+
for (const segment of segments) {
|
|
118
|
+
const entity = entities[currentType]
|
|
119
|
+
if (!entity) {
|
|
120
|
+
return rootType
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const inclusion = entity.inclusions?.find(inc => inc.field === segment)
|
|
124
|
+
if (!inclusion) {
|
|
125
|
+
return rootType
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
currentType = inclusion.type
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return currentType
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveTypeByImplicitInclusion(
|
|
135
|
+
rootType: string,
|
|
136
|
+
fallbackType: string,
|
|
137
|
+
entities: Readonly<Record<string, EntityModel>> | undefined,
|
|
138
|
+
): string {
|
|
139
|
+
if (!entities) {
|
|
140
|
+
return rootType
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (rootType === fallbackType) {
|
|
144
|
+
return rootType
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rootEntity = entities[rootType]
|
|
148
|
+
if (!rootEntity) {
|
|
149
|
+
return rootType
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (rootEntity.extensions?.includes(fallbackType)) {
|
|
153
|
+
return rootType
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const inclusion = rootEntity.inclusions?.find(inc => inc.type === fallbackType)
|
|
157
|
+
if (inclusion) {
|
|
158
|
+
return fallbackType
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return rootType
|
|
162
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ComponentModel,
|
|
3
|
+
type EntityModel,
|
|
3
4
|
type HubInput,
|
|
4
5
|
type HubModel,
|
|
5
6
|
type InstanceInput,
|
|
6
7
|
type InstanceModel,
|
|
8
|
+
inputKey,
|
|
7
9
|
isUnitModel,
|
|
8
10
|
} from "@highstate/contract"
|
|
9
11
|
import { fromEntries, mapValues } from "remeda"
|
|
12
|
+
import { resolveEffectiveOutputType } from "./effective-output-type"
|
|
10
13
|
import { GraphResolver } from "./graph-resolver"
|
|
11
14
|
|
|
12
15
|
export type InputResolverNode =
|
|
@@ -14,10 +17,12 @@ export type InputResolverNode =
|
|
|
14
17
|
kind: "instance"
|
|
15
18
|
instance: InstanceModel
|
|
16
19
|
component: ComponentModel
|
|
20
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
17
21
|
}
|
|
18
22
|
| {
|
|
19
23
|
kind: "hub"
|
|
20
24
|
hub: HubModel
|
|
25
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export type ResolvedInstanceInput = {
|
|
@@ -30,6 +35,7 @@ export type InputResolverOutput =
|
|
|
30
35
|
kind: "instance"
|
|
31
36
|
instance: InstanceModel
|
|
32
37
|
component: ComponentModel
|
|
38
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
33
39
|
resolvedInputs: Record<string, ResolvedInstanceInput[]>
|
|
34
40
|
resolvedOutputs: Record<string, InstanceInput[]> | undefined
|
|
35
41
|
resolvedInjectionInputs: ResolvedInstanceInput[]
|
|
@@ -37,6 +43,7 @@ export type InputResolverOutput =
|
|
|
37
43
|
}
|
|
38
44
|
| {
|
|
39
45
|
kind: "hub"
|
|
46
|
+
hub: HubModel
|
|
40
47
|
resolvedInputs: ResolvedInstanceInput[]
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -78,6 +85,25 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
78
85
|
return dependencies
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
private resolveOutputTypeForInput(input: InstanceInput, fallbackType: string): string {
|
|
89
|
+
return resolveEffectiveOutputType({
|
|
90
|
+
input,
|
|
91
|
+
fallbackType,
|
|
92
|
+
getInstanceContext: instanceId => {
|
|
93
|
+
const output = this.outputs.get(`instance:${instanceId}`)
|
|
94
|
+
if (!output || output.kind !== "instance") {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
instance: output.instance,
|
|
100
|
+
component: output.component,
|
|
101
|
+
entities: output.entities,
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
processNode(node: InputResolverNode): InputResolverOutput {
|
|
82
108
|
const getHubOutput = (input: HubInput) => {
|
|
83
109
|
const output = this.outputs.get(`hub:${input.hubId}`)
|
|
@@ -118,7 +144,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
118
144
|
const hubResult: Map<string, ResolvedInstanceInput> = new Map()
|
|
119
145
|
|
|
120
146
|
const addHubResult = (input: ResolvedInstanceInput) => {
|
|
121
|
-
hubResult.set(
|
|
147
|
+
hubResult.set(inputKey(input.input), input)
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
for (const input of node.hub.inputs ?? []) {
|
|
@@ -130,7 +156,10 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
130
156
|
continue
|
|
131
157
|
}
|
|
132
158
|
|
|
133
|
-
addHubResult({
|
|
159
|
+
addHubResult({
|
|
160
|
+
input,
|
|
161
|
+
type: this.resolveOutputTypeForInput(input, componentInput.type),
|
|
162
|
+
})
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
for (const injectionInput of node.hub.injectionInputs ?? []) {
|
|
@@ -143,6 +172,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
143
172
|
|
|
144
173
|
return {
|
|
145
174
|
kind: "hub",
|
|
175
|
+
hub: node.hub,
|
|
146
176
|
resolvedInputs: Array.from(hubResult.values()),
|
|
147
177
|
}
|
|
148
178
|
}
|
|
@@ -160,6 +190,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
160
190
|
kind: "instance",
|
|
161
191
|
instance: node.instance,
|
|
162
192
|
component: node.component,
|
|
193
|
+
entities: node.entities,
|
|
163
194
|
resolvedInputs: mapValues(node.instance.resolvedInputs, (inputs, inputName) => {
|
|
164
195
|
const componentInput = node.component.inputs[inputName]
|
|
165
196
|
if (!componentInput) {
|
|
@@ -189,7 +220,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
189
220
|
resolvedInputsMap.set(inputName, inputs)
|
|
190
221
|
}
|
|
191
222
|
|
|
192
|
-
inputs.set(
|
|
223
|
+
inputs.set(inputKey(input.input), input)
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
const addInstanceInput = (inputName: string, input: InstanceInput) => {
|
|
@@ -211,17 +242,37 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
211
242
|
}
|
|
212
243
|
|
|
213
244
|
if (isUnitModel(component)) {
|
|
214
|
-
addInstanceResult(inputName, {
|
|
245
|
+
addInstanceResult(inputName, {
|
|
246
|
+
input,
|
|
247
|
+
type: this.resolveOutputTypeForInput(input, componentInput.type),
|
|
248
|
+
})
|
|
215
249
|
return
|
|
216
250
|
}
|
|
217
251
|
|
|
218
252
|
if (resolvedOutputs) {
|
|
219
253
|
for (const output of resolvedOutputs) {
|
|
220
|
-
addInstanceResult(inputName, {
|
|
254
|
+
addInstanceResult(inputName, {
|
|
255
|
+
input: {
|
|
256
|
+
...output,
|
|
257
|
+
// keep explicit path from the edge while preserving already-resolved output path
|
|
258
|
+
path: input.path ?? output.path,
|
|
259
|
+
},
|
|
260
|
+
type: this.resolveOutputTypeForInput(
|
|
261
|
+
{
|
|
262
|
+
instanceId: input.instanceId,
|
|
263
|
+
output: input.output,
|
|
264
|
+
path: input.path ?? output.path,
|
|
265
|
+
},
|
|
266
|
+
componentInput.type,
|
|
267
|
+
),
|
|
268
|
+
})
|
|
221
269
|
}
|
|
222
270
|
} else {
|
|
223
271
|
// if the instance is not evaluated, we a forced to use the input as is
|
|
224
|
-
addInstanceResult(inputName, {
|
|
272
|
+
addInstanceResult(inputName, {
|
|
273
|
+
input,
|
|
274
|
+
type: this.resolveOutputTypeForInput(input, componentInput.type),
|
|
275
|
+
})
|
|
225
276
|
}
|
|
226
277
|
}
|
|
227
278
|
|
|
@@ -237,7 +288,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
237
288
|
for (const injectionInput of node.instance.injectionInputs ?? []) {
|
|
238
289
|
const { resolvedInputs } = getHubOutput(injectionInput)
|
|
239
290
|
for (const input of resolvedInputs) {
|
|
240
|
-
injectionInputs.set(
|
|
291
|
+
injectionInputs.set(inputKey(input.input), input)
|
|
241
292
|
}
|
|
242
293
|
}
|
|
243
294
|
|
|
@@ -248,7 +299,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
248
299
|
for (const hubInput of hubInputs) {
|
|
249
300
|
const { resolvedInputs } = getHubOutput(hubInput)
|
|
250
301
|
for (const input of resolvedInputs) {
|
|
251
|
-
allInputs.set(
|
|
302
|
+
allInputs.set(inputKey(input.input), input)
|
|
252
303
|
}
|
|
253
304
|
}
|
|
254
305
|
|
|
@@ -256,7 +307,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
256
307
|
if (input.type === componentInput.type) {
|
|
257
308
|
addInstanceInput(inputName, input.input)
|
|
258
309
|
|
|
259
|
-
const key =
|
|
310
|
+
const key = inputKey(input.input)
|
|
260
311
|
if (injectionInputs.has(key)) {
|
|
261
312
|
matchedInjectionInputs.set(key, input)
|
|
262
313
|
}
|
|
@@ -275,6 +326,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
275
326
|
kind: "instance",
|
|
276
327
|
instance: node.instance,
|
|
277
328
|
component: node.component,
|
|
329
|
+
entities: node.entities,
|
|
278
330
|
resolvedInputs,
|
|
279
331
|
resolvedOutputs: node.instance.resolvedOutputs,
|
|
280
332
|
resolvedInjectionInputs: Array.from(injectionInputs.values()),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function stableJsonStringify(value: unknown): string {
|
|
2
|
+
if (value === null) {
|
|
3
|
+
return "null"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return `[${value.map(stableJsonStringify).join(",")}]`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
switch (typeof value) {
|
|
11
|
+
case "string":
|
|
12
|
+
return JSON.stringify(value)
|
|
13
|
+
case "number": {
|
|
14
|
+
if (!Number.isFinite(value)) {
|
|
15
|
+
throw new Error("Snapshot content contains a non-finite number")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.stringify(value)
|
|
19
|
+
}
|
|
20
|
+
case "boolean":
|
|
21
|
+
return value ? "true" : "false"
|
|
22
|
+
case "object": {
|
|
23
|
+
const record = value as Record<string, unknown>
|
|
24
|
+
const keys = Object.keys(record).sort()
|
|
25
|
+
const parts: string[] = []
|
|
26
|
+
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const item = record[key]
|
|
29
|
+
if (item === undefined) {
|
|
30
|
+
throw new Error("Snapshot content contains undefined")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parts.push(`${JSON.stringify(key)}:${stableJsonStringify(item)}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `{${parts.join(",")}}`
|
|
37
|
+
}
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Snapshot content contains non-JSON value of type "${typeof value}"`)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/terminal/manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Logger } from "pino"
|
|
2
2
|
import type { ProjectUnlockService } from "../business"
|
|
3
|
+
import type { ObjectRefIndexService } from "../business/object-ref-index"
|
|
3
4
|
import type { DatabaseManager, Terminal, TerminalSession } from "../database"
|
|
4
5
|
import type { PubSubManager } from "../pubsub"
|
|
5
6
|
import type { TerminalSessionOutput } from "../shared/models/project/terminal"
|
|
@@ -35,6 +36,7 @@ export class TerminalManager {
|
|
|
35
36
|
private readonly database: DatabaseManager,
|
|
36
37
|
private readonly pubsubManager: PubSubManager,
|
|
37
38
|
private readonly projectUnlockService: ProjectUnlockService,
|
|
39
|
+
private readonly objectRefIndexService: ObjectRefIndexService,
|
|
38
40
|
private readonly logger: Logger,
|
|
39
41
|
) {
|
|
40
42
|
this.projectUnlockService.registerUnlockTask(
|
|
@@ -113,6 +115,8 @@ export class TerminalManager {
|
|
|
113
115
|
},
|
|
114
116
|
})
|
|
115
117
|
|
|
118
|
+
await this.objectRefIndexService.track(projectId, [session.id])
|
|
119
|
+
|
|
116
120
|
const output = toTerminalSessionOutput(terminal, session)
|
|
117
121
|
|
|
118
122
|
this.logger.info({ msg: "terminal session created", id: output.id })
|
|
@@ -319,6 +323,7 @@ export class TerminalManager {
|
|
|
319
323
|
database: DatabaseManager,
|
|
320
324
|
pubsubManager: PubSubManager,
|
|
321
325
|
projectUnlockService: ProjectUnlockService,
|
|
326
|
+
objectRefIndexService: ObjectRefIndexService,
|
|
322
327
|
logger: Logger,
|
|
323
328
|
): TerminalManager {
|
|
324
329
|
return new TerminalManager(
|
|
@@ -326,6 +331,7 @@ export class TerminalManager {
|
|
|
326
331
|
database,
|
|
327
332
|
pubsubManager,
|
|
328
333
|
projectUnlockService,
|
|
334
|
+
objectRefIndexService,
|
|
329
335
|
logger.child({ service: "TerminalManager" }),
|
|
330
336
|
)
|
|
331
337
|
}
|
package/src/worker/manager.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Logger } from "pino"
|
|
|
2
2
|
import type { ApiKeyService, ProjectUnlockService } from "../business"
|
|
3
3
|
import type { DatabaseManager, Worker, WorkerVersion } from "../database"
|
|
4
4
|
import type { PubSubManager } from "../pubsub"
|
|
5
|
+
import type { WorkerVersionStatus } from "../shared"
|
|
5
6
|
import type { WorkerBackend } from "./abstractions"
|
|
6
7
|
import { PassThrough } from "node:stream"
|
|
7
8
|
import { z } from "zod"
|
|
@@ -9,6 +10,9 @@ import { type AsyncBatcher, createAsyncBatcher } from "../shared"
|
|
|
9
10
|
|
|
10
11
|
export const workerManagerConfig = z.object({
|
|
11
12
|
HIGHSTATE_WORKER_API_PATH: z.string().default("/var/run/highstate.sock"),
|
|
13
|
+
HIGHSTATE_WORKER_START_MAX_ATTEMPTS: z.coerce.number().int().positive().default(5),
|
|
14
|
+
HIGHSTATE_WORKER_RESTART_BACKOFF_BASE_MS: z.coerce.number().int().nonnegative().default(1000),
|
|
15
|
+
HIGHSTATE_WORKER_RESTART_BACKOFF_MAX_MS: z.coerce.number().int().nonnegative().default(30000),
|
|
12
16
|
})
|
|
13
17
|
|
|
14
18
|
type RunningWorkerInfo = {
|
|
@@ -22,7 +26,40 @@ type RunningWorkerInfo = {
|
|
|
22
26
|
lineBuffer?: string
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
function getWorkerRestartBackoffMs(failedAttempts: number, baseMs: number, maxMs: number): number {
|
|
30
|
+
if (failedAttempts <= 0) {
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const exponent = Math.max(0, failedAttempts - 1)
|
|
35
|
+
const uncapped = baseMs * 2 ** exponent
|
|
36
|
+
|
|
37
|
+
return Math.min(maxMs, uncapped)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function waitForAbortableDelay(delayMs: number, signal: AbortSignal): Promise<boolean> {
|
|
41
|
+
if (delayMs <= 0) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (signal.aborted) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return await new Promise(resolve => {
|
|
50
|
+
const onAbort = () => {
|
|
51
|
+
clearTimeout(timer)
|
|
52
|
+
resolve(false)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
signal.removeEventListener("abort", onAbort)
|
|
57
|
+
resolve(true)
|
|
58
|
+
}, delayMs)
|
|
59
|
+
|
|
60
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
61
|
+
})
|
|
62
|
+
}
|
|
26
63
|
|
|
27
64
|
export class WorkerManager {
|
|
28
65
|
constructor(
|
|
@@ -50,6 +87,17 @@ export class WorkerManager {
|
|
|
50
87
|
|
|
51
88
|
private readonly runningWorkers = new Map<string, RunningWorkerInfo>()
|
|
52
89
|
|
|
90
|
+
private async publishWorkerVersionStatus(
|
|
91
|
+
projectId: string,
|
|
92
|
+
workerVersionId: string,
|
|
93
|
+
status: WorkerVersionStatus,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
await this.pubsubManager.publish(["worker-version-status", projectId], {
|
|
96
|
+
workerVersionId,
|
|
97
|
+
status,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
53
101
|
private async startWorkerVersion(
|
|
54
102
|
projectId: string,
|
|
55
103
|
workerVersion: WorkerVersion & { worker: Worker },
|
|
@@ -70,6 +118,7 @@ export class WorkerManager {
|
|
|
70
118
|
// calculate attempt number
|
|
71
119
|
const previousFailedAttempts = existingInfo?.failedAttempts ?? 0
|
|
72
120
|
const failedAttempts = restart ? previousFailedAttempts + 1 : 0
|
|
121
|
+
const maxWorkerStartAttempts = this.config.HIGHSTATE_WORKER_START_MAX_ATTEMPTS
|
|
73
122
|
|
|
74
123
|
// check if max attempts reached
|
|
75
124
|
if (failedAttempts >= maxWorkerStartAttempts) {
|
|
@@ -95,6 +144,8 @@ export class WorkerManager {
|
|
|
95
144
|
},
|
|
96
145
|
})
|
|
97
146
|
|
|
147
|
+
await this.publishWorkerVersionStatus(projectId, workerVersion.id, "error")
|
|
148
|
+
|
|
98
149
|
// clean up from running workers map
|
|
99
150
|
if (existingInfo) {
|
|
100
151
|
existingInfo.logBatcher && void existingInfo.logBatcher.flush()
|
|
@@ -104,6 +155,45 @@ export class WorkerManager {
|
|
|
104
155
|
return
|
|
105
156
|
}
|
|
106
157
|
|
|
158
|
+
if (restart && existingInfo) {
|
|
159
|
+
const restartBackoffMs = getWorkerRestartBackoffMs(
|
|
160
|
+
failedAttempts,
|
|
161
|
+
this.config.HIGHSTATE_WORKER_RESTART_BACKOFF_BASE_MS,
|
|
162
|
+
this.config.HIGHSTATE_WORKER_RESTART_BACKOFF_MAX_MS,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (restartBackoffMs > 0) {
|
|
166
|
+
this.logger.debug(
|
|
167
|
+
{ projectId, workerVersionId: workerVersion.id, restartBackoffMs, failedAttempts },
|
|
168
|
+
`delaying worker version "%s" restart for %s ms after attempt %s`,
|
|
169
|
+
workerVersion.id,
|
|
170
|
+
restartBackoffMs,
|
|
171
|
+
failedAttempts,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await this.writeWorkerLog(
|
|
175
|
+
projectId,
|
|
176
|
+
workerVersion.id,
|
|
177
|
+
`worker restart scheduled in ${restartBackoffMs}ms`,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const canContinue = await waitForAbortableDelay(
|
|
181
|
+
restartBackoffMs,
|
|
182
|
+
existingInfo.abortController.signal,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (!canContinue) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const currentInfo = this.runningWorkers.get(workerVersion.id)
|
|
190
|
+
|
|
191
|
+
if (currentInfo !== existingInfo || currentInfo.abortController.signal.aborted) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
107
197
|
// regenerate API token
|
|
108
198
|
const apiKey = await this.apiKeyService.regenerateToken(projectId, workerVersion.apiKeyId)
|
|
109
199
|
const stdout = new PassThrough()
|
|
@@ -184,6 +274,8 @@ export class WorkerManager {
|
|
|
184
274
|
},
|
|
185
275
|
})
|
|
186
276
|
|
|
277
|
+
await this.publishWorkerVersionStatus(projectId, workerVersion.id, "starting")
|
|
278
|
+
|
|
187
279
|
void this.workerBackend
|
|
188
280
|
.run({
|
|
189
281
|
projectId,
|
|
@@ -272,6 +364,8 @@ export class WorkerManager {
|
|
|
272
364
|
},
|
|
273
365
|
})
|
|
274
366
|
|
|
367
|
+
await this.publishWorkerVersionStatus(projectId, workerVersionId, "running")
|
|
368
|
+
|
|
275
369
|
this.logger.debug(
|
|
276
370
|
{ projectId, workerVersionId },
|
|
277
371
|
`worker version "%s" is now running in project "%s"`,
|
|
@@ -311,6 +405,8 @@ export class WorkerManager {
|
|
|
311
405
|
},
|
|
312
406
|
})
|
|
313
407
|
|
|
408
|
+
await this.publishWorkerVersionStatus(info.projectId, workerVersionId, "stopped")
|
|
409
|
+
|
|
314
410
|
this.logger.debug(
|
|
315
411
|
{ projectId: info.projectId, workerVersionId },
|
|
316
412
|
`stopped worker version "%s"`,
|