@highstate/backend 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +498 -338
- package/dist/index.mjs +2155 -513
- package/dist/input-hash-C8HEDMjz.mjs +292 -0
- package/dist/library/worker/main.mjs +169 -0
- package/dist/operation-8k4Tv4dw.d.ts +841 -0
- package/dist/shared/index.d.ts +21 -0
- package/dist/shared/index.mjs +4 -0
- package/package.json +20 -9
- package/dist/library/workers/evaluator.mjs +0 -43
- package/dist/library/workers/loader.mjs +0 -11
- package/dist/shared-B55c8XjT.mjs +0 -36
@@ -0,0 +1,292 @@
|
|
1
|
+
import { z } from 'zod';
|
2
|
+
import '@highstate/contract';
|
3
|
+
import { sha256 } from 'crypto-hash';
|
4
|
+
|
5
|
+
const positionSchema = z.object({
|
6
|
+
x: z.number(),
|
7
|
+
y: z.number()
|
8
|
+
});
|
9
|
+
const instanceInputSchema = z.object({
|
10
|
+
type: z.string(),
|
11
|
+
instanceId: z.string(),
|
12
|
+
output: z.string()
|
13
|
+
});
|
14
|
+
const instanceInputMapSchema = z.record(
|
15
|
+
z.union([instanceInputSchema, z.array(instanceInputSchema)])
|
16
|
+
);
|
17
|
+
const instanceModelSchema = z.object({
|
18
|
+
id: z.string(),
|
19
|
+
parentId: z.string().optional(),
|
20
|
+
type: z.string(),
|
21
|
+
name: z.string(),
|
22
|
+
position: positionSchema.optional(),
|
23
|
+
args: z.record(z.unknown()),
|
24
|
+
inputs: instanceInputMapSchema,
|
25
|
+
outputs: instanceInputMapSchema.optional()
|
26
|
+
});
|
27
|
+
const compositeInstanceSchema = z.object({
|
28
|
+
instance: instanceModelSchema,
|
29
|
+
children: z.array(instanceModelSchema)
|
30
|
+
});
|
31
|
+
|
32
|
+
const instanceStatusSchema = z.enum([
|
33
|
+
"not_created",
|
34
|
+
"updating",
|
35
|
+
"destroying",
|
36
|
+
"refreshing",
|
37
|
+
"created",
|
38
|
+
"error",
|
39
|
+
"pending",
|
40
|
+
"unknown"
|
41
|
+
]);
|
42
|
+
const instanceStatusFieldSchema = z.object({
|
43
|
+
value: z.string().optional(),
|
44
|
+
sensitive: z.boolean().optional(),
|
45
|
+
displayName: z.string().optional()
|
46
|
+
});
|
47
|
+
const instanceStatusFieldMapSchema = z.record(instanceStatusFieldSchema);
|
48
|
+
const instanceRepresentationMetaSchema = z.object({
|
49
|
+
showQRCode: z.boolean().optional(),
|
50
|
+
contentType: z.string().optional(),
|
51
|
+
fileName: z.string().optional()
|
52
|
+
});
|
53
|
+
const instanceRepresentationSchema = z.object({
|
54
|
+
...instanceRepresentationMetaSchema.shape,
|
55
|
+
content: z.string()
|
56
|
+
});
|
57
|
+
const terminalFactorySchema = z.object({
|
58
|
+
image: z.string(),
|
59
|
+
command: z.string().array(),
|
60
|
+
cwd: z.string().optional(),
|
61
|
+
env: z.record(z.string()).optional(),
|
62
|
+
files: z.record(z.string()).optional()
|
63
|
+
});
|
64
|
+
const instanceStatePatchSchema = z.object({
|
65
|
+
id: z.string(),
|
66
|
+
status: instanceStatusSchema.optional(),
|
67
|
+
latestOperationId: z.string().nullable().optional(),
|
68
|
+
currentResourceCount: z.number().nullable().optional(),
|
69
|
+
totalResourceCount: z.number().nullable().optional(),
|
70
|
+
dependentKeys: z.array(z.string()).nullable().optional(),
|
71
|
+
inputHash: z.string().nullable().optional(),
|
72
|
+
error: z.string().nullable().optional(),
|
73
|
+
message: z.string().nullable().optional(),
|
74
|
+
// secrets updated by the unit
|
75
|
+
secrets: z.record(z.string()).nullable().optional(),
|
76
|
+
statusFields: instanceStatusFieldMapSchema.nullable().optional(),
|
77
|
+
representationMeta: instanceRepresentationMetaSchema.nullable().optional(),
|
78
|
+
hasTerminal: z.boolean().optional()
|
79
|
+
});
|
80
|
+
const instanceStateSchema = z.object({
|
81
|
+
id: z.string(),
|
82
|
+
parentId: z.string().nullable(),
|
83
|
+
status: instanceStatusSchema,
|
84
|
+
latestOperationId: z.string().nullable(),
|
85
|
+
currentResourceCount: z.number().nullable(),
|
86
|
+
totalResourceCount: z.number().nullable(),
|
87
|
+
dependentKeys: z.array(z.string()).nullable(),
|
88
|
+
inputHash: z.string().nullable(),
|
89
|
+
error: z.string().nullable(),
|
90
|
+
statusFields: instanceStatusFieldMapSchema.nullable(),
|
91
|
+
representationMeta: instanceRepresentationMetaSchema.nullable(),
|
92
|
+
hasTerminal: z.boolean()
|
93
|
+
});
|
94
|
+
function createInstanceState(id, status = "not_created", fields = {}) {
|
95
|
+
return {
|
96
|
+
id,
|
97
|
+
parentId: null,
|
98
|
+
status,
|
99
|
+
latestOperationId: null,
|
100
|
+
currentResourceCount: null,
|
101
|
+
totalResourceCount: null,
|
102
|
+
dependentKeys: null,
|
103
|
+
inputHash: null,
|
104
|
+
error: null,
|
105
|
+
statusFields: null,
|
106
|
+
representationMeta: null,
|
107
|
+
hasTerminal: false,
|
108
|
+
...fields
|
109
|
+
};
|
110
|
+
}
|
111
|
+
function applyInstanceStatePatch(states, patch) {
|
112
|
+
let state = states.get(patch.id);
|
113
|
+
if (!state) {
|
114
|
+
state = createInstanceState(patch.id, "unknown");
|
115
|
+
states.set(patch.id, state);
|
116
|
+
}
|
117
|
+
if (patch.status !== undefined) {
|
118
|
+
state.status = patch.status;
|
119
|
+
}
|
120
|
+
if (patch.latestOperationId !== undefined) {
|
121
|
+
state.latestOperationId = patch.latestOperationId;
|
122
|
+
}
|
123
|
+
if (patch.currentResourceCount !== undefined) {
|
124
|
+
state.currentResourceCount = patch.currentResourceCount;
|
125
|
+
}
|
126
|
+
if (patch.totalResourceCount !== undefined) {
|
127
|
+
state.totalResourceCount = patch.totalResourceCount;
|
128
|
+
}
|
129
|
+
if (patch.dependentKeys !== undefined) {
|
130
|
+
state.dependentKeys = patch.dependentKeys;
|
131
|
+
}
|
132
|
+
if (patch.inputHash !== undefined) {
|
133
|
+
state.inputHash = patch.inputHash;
|
134
|
+
}
|
135
|
+
if (patch.error !== undefined) {
|
136
|
+
state.error = patch.error;
|
137
|
+
}
|
138
|
+
if (patch.statusFields !== undefined) {
|
139
|
+
state.statusFields = patch.statusFields;
|
140
|
+
}
|
141
|
+
if (patch.representationMeta !== undefined) {
|
142
|
+
state.representationMeta = patch.representationMeta;
|
143
|
+
}
|
144
|
+
if (patch.hasTerminal !== undefined) {
|
145
|
+
state.hasTerminal = patch.hasTerminal;
|
146
|
+
}
|
147
|
+
return state;
|
148
|
+
}
|
149
|
+
function applyInstanceStateFrontendPatch(states, patch) {
|
150
|
+
if (!patch.id) {
|
151
|
+
throw new Error("The ID of the instance state is required.");
|
152
|
+
}
|
153
|
+
let state = states.get(patch.id);
|
154
|
+
if (!state) {
|
155
|
+
state = createInstanceState(patch.id, "unknown");
|
156
|
+
states.set(patch.id, state);
|
157
|
+
}
|
158
|
+
if (patch.status !== undefined) {
|
159
|
+
state.status = patch.status;
|
160
|
+
}
|
161
|
+
if (patch.latestOperationId !== undefined) {
|
162
|
+
state.latestOperationId = patch.latestOperationId;
|
163
|
+
}
|
164
|
+
if (patch.currentResourceCount !== undefined) {
|
165
|
+
state.currentResourceCount = patch.currentResourceCount;
|
166
|
+
}
|
167
|
+
if (patch.totalResourceCount !== undefined) {
|
168
|
+
state.totalResourceCount = patch.totalResourceCount;
|
169
|
+
}
|
170
|
+
if (patch.dependentKeys !== undefined) {
|
171
|
+
state.dependentKeys = patch.dependentKeys;
|
172
|
+
}
|
173
|
+
if (patch.inputHash !== undefined) {
|
174
|
+
state.inputHash = patch.inputHash;
|
175
|
+
}
|
176
|
+
if (patch.error !== undefined) {
|
177
|
+
state.error = patch.error;
|
178
|
+
}
|
179
|
+
if (patch.statusFields !== undefined) {
|
180
|
+
state.statusFields = patch.statusFields;
|
181
|
+
}
|
182
|
+
if (patch.representationMeta !== undefined) {
|
183
|
+
state.representationMeta = patch.representationMeta;
|
184
|
+
}
|
185
|
+
if (patch.hasTerminal !== undefined) {
|
186
|
+
state.hasTerminal = patch.hasTerminal;
|
187
|
+
}
|
188
|
+
return state;
|
189
|
+
}
|
190
|
+
function createInstanceStateFrontendPatch(patch) {
|
191
|
+
return {
|
192
|
+
id: patch.id,
|
193
|
+
status: patch.status,
|
194
|
+
latestOperationId: patch.latestOperationId,
|
195
|
+
currentResourceCount: patch.currentResourceCount,
|
196
|
+
totalResourceCount: patch.totalResourceCount,
|
197
|
+
dependentKeys: patch.dependentKeys,
|
198
|
+
inputHash: patch.inputHash,
|
199
|
+
error: patch.error,
|
200
|
+
statusFields: patch.statusFields,
|
201
|
+
representationMeta: patch.representationMeta,
|
202
|
+
hasTerminal: patch.hasTerminal
|
203
|
+
};
|
204
|
+
}
|
205
|
+
|
206
|
+
const operationTypeSchema = z.enum(["update", "destroy", "recreate", "refresh", "evaluate"]);
|
207
|
+
const operationStatusSchema = z.enum([
|
208
|
+
"pending",
|
209
|
+
"evaluating",
|
210
|
+
"running",
|
211
|
+
"completed",
|
212
|
+
"failed",
|
213
|
+
"cancelled"
|
214
|
+
]);
|
215
|
+
const projectOperationSchema = z.object({
|
216
|
+
id: z.string().uuid(),
|
217
|
+
projectId: z.string(),
|
218
|
+
type: operationTypeSchema,
|
219
|
+
status: operationStatusSchema,
|
220
|
+
instanceIds: z.array(z.string()),
|
221
|
+
error: z.string().nullable(),
|
222
|
+
startedAt: z.number(),
|
223
|
+
completedAt: z.number().nullable()
|
224
|
+
});
|
225
|
+
const projectOperationRequestSchema = z.object({
|
226
|
+
projectId: z.string(),
|
227
|
+
type: operationTypeSchema,
|
228
|
+
instanceIds: z.array(z.string())
|
229
|
+
});
|
230
|
+
|
231
|
+
class InputHashCalculator {
|
232
|
+
constructor(instances) {
|
233
|
+
this.instances = instances;
|
234
|
+
}
|
235
|
+
cache = /* @__PURE__ */ new Map();
|
236
|
+
promiseCache = /* @__PURE__ */ new Map();
|
237
|
+
reset(instanceId) {
|
238
|
+
this.cache.delete(instanceId);
|
239
|
+
}
|
240
|
+
async calculate(instance) {
|
241
|
+
const entry = await this.calculateFull(instance);
|
242
|
+
return entry.inputHash;
|
243
|
+
}
|
244
|
+
async calculateFull(instance) {
|
245
|
+
const existingPromise = this.promiseCache.get(instance.id);
|
246
|
+
if (existingPromise) {
|
247
|
+
return existingPromise;
|
248
|
+
}
|
249
|
+
const promise = this._calculate(instance).finally(() => this.promiseCache.delete(instance.id));
|
250
|
+
this.promiseCache.set(instance.id, promise);
|
251
|
+
return promise;
|
252
|
+
}
|
253
|
+
async _calculate(instance) {
|
254
|
+
let entry = this.cache.get(instance.id);
|
255
|
+
if (entry) return entry;
|
256
|
+
if (entry === null) {
|
257
|
+
throw new Error(`Circular dependency detected: ${instance.id}`);
|
258
|
+
}
|
259
|
+
this.cache.set(instance.id, null);
|
260
|
+
let sink = JSON.stringify(instance.args);
|
261
|
+
const dependencies = [];
|
262
|
+
const updateWithInput = async (inputName, input) => {
|
263
|
+
const inputInstance = this.instances.get(input.instanceId);
|
264
|
+
if (!inputInstance) {
|
265
|
+
throw new Error(`Instance not found: ${input.instanceId}`);
|
266
|
+
}
|
267
|
+
const inputEntry = await this.calculateFull(inputInstance);
|
268
|
+
inputEntry.dependents.push(inputInstance.id);
|
269
|
+
dependencies.push(inputInstance.id);
|
270
|
+
sink += inputName;
|
271
|
+
sink += inputEntry.inputHash;
|
272
|
+
};
|
273
|
+
for (const [inputName, input] of Object.entries(instance.inputs)) {
|
274
|
+
if (!Array.isArray(input)) {
|
275
|
+
await updateWithInput(inputName, input);
|
276
|
+
continue;
|
277
|
+
}
|
278
|
+
for (const inputRef of input) {
|
279
|
+
await updateWithInput(inputName, inputRef);
|
280
|
+
}
|
281
|
+
}
|
282
|
+
entry = {
|
283
|
+
inputHash: await sha256(sink),
|
284
|
+
dependents: [],
|
285
|
+
dependencies
|
286
|
+
};
|
287
|
+
this.cache.set(instance.id, entry);
|
288
|
+
return entry;
|
289
|
+
}
|
290
|
+
}
|
291
|
+
|
292
|
+
export { InputHashCalculator as I, instanceInputMapSchema as a, instanceModelSchema as b, compositeInstanceSchema as c, instanceStatusSchema as d, instanceStatusFieldSchema as e, instanceStatusFieldMapSchema as f, instanceRepresentationMetaSchema as g, instanceRepresentationSchema as h, instanceInputSchema as i, instanceStatePatchSchema as j, instanceStateSchema as k, createInstanceState as l, applyInstanceStatePatch as m, applyInstanceStateFrontendPatch as n, createInstanceStateFrontendPatch as o, positionSchema as p, operationTypeSchema as q, operationStatusSchema as r, projectOperationSchema as s, terminalFactorySchema as t, projectOperationRequestSchema as u };
|
@@ -0,0 +1,169 @@
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
2
|
+
import { createJiti } from 'jiti';
|
3
|
+
import { pino } from 'pino';
|
4
|
+
import { mapValues } from 'remeda';
|
5
|
+
import { isComponent, isEntity, resetEvaluation, getInstances, getCompositeInstances } from '@highstate/contract';
|
6
|
+
|
7
|
+
async function loadLibrary(jiti, logger, modulePaths) {
|
8
|
+
const modules = {};
|
9
|
+
for (const modulePath of modulePaths) {
|
10
|
+
try {
|
11
|
+
logger.debug("loading module", { modulePath });
|
12
|
+
modules[modulePath] = await jiti.import(modulePath);
|
13
|
+
logger.debug("module loaded", { modulePath });
|
14
|
+
} catch (error) {
|
15
|
+
logger.error("module load failed", { modulePath, error });
|
16
|
+
}
|
17
|
+
}
|
18
|
+
const components = {};
|
19
|
+
const entities = {};
|
20
|
+
_loadLibrary(modules, components, entities);
|
21
|
+
logger.info("library loaded", {
|
22
|
+
componentCount: Object.keys(components).length,
|
23
|
+
entityCount: Object.keys(entities).length
|
24
|
+
});
|
25
|
+
logger.trace("library content", { components, entities });
|
26
|
+
return { components, entities };
|
27
|
+
}
|
28
|
+
function _loadLibrary(value, components, entities) {
|
29
|
+
if (isComponent(value)) {
|
30
|
+
components[value.model.type] = value;
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
if (isEntity(value)) {
|
34
|
+
entities[value.type] = value;
|
35
|
+
return;
|
36
|
+
}
|
37
|
+
if (typeof value !== "object" || value === null) {
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
for (const key in value) {
|
41
|
+
_loadLibrary(value[key], components, entities);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
async function evaluateModules(jiti, logger, modulePaths) {
|
46
|
+
resetEvaluation();
|
47
|
+
for (const modulePath of modulePaths) {
|
48
|
+
try {
|
49
|
+
logger.debug("loading module", { modulePath });
|
50
|
+
await jiti.import(modulePath);
|
51
|
+
logger.debug("module loaded", { modulePath });
|
52
|
+
} catch (error) {
|
53
|
+
logger.error("module load failed", { modulePath, error });
|
54
|
+
}
|
55
|
+
}
|
56
|
+
return getInstances();
|
57
|
+
}
|
58
|
+
function evaluateInstances(logger, library, allInstances, instanceIds) {
|
59
|
+
resetEvaluation();
|
60
|
+
const allInstancesMap = new Map(allInstances.map((instance) => [instance.id, instance]));
|
61
|
+
const instanceOutputs = /* @__PURE__ */ new Map();
|
62
|
+
for (const instanceId of instanceIds ?? allInstancesMap.keys()) {
|
63
|
+
logger.debug("evaluating top-level instance", { instanceId });
|
64
|
+
evaluateInstance(instanceId);
|
65
|
+
}
|
66
|
+
return getCompositeInstances();
|
67
|
+
function evaluateInstance(instanceId) {
|
68
|
+
let outputs = instanceOutputs.get(instanceId);
|
69
|
+
if (!outputs) {
|
70
|
+
outputs = _evaluateInstance(instanceId);
|
71
|
+
instanceOutputs.set(instanceId, outputs);
|
72
|
+
}
|
73
|
+
return outputs;
|
74
|
+
}
|
75
|
+
function _evaluateInstance(instanceId) {
|
76
|
+
const inputs = {};
|
77
|
+
const instance = allInstancesMap.get(instanceId);
|
78
|
+
logger.info("evaluating instance", { instanceId });
|
79
|
+
if (!instance) {
|
80
|
+
throw new Error(`Instance not found: ${instanceId}`);
|
81
|
+
}
|
82
|
+
for (const [inputName, input] of Object.entries(instance.inputs)) {
|
83
|
+
if (!Array.isArray(input)) {
|
84
|
+
const outputs = evaluateInstance(input.instanceId);
|
85
|
+
inputs[inputName] = outputs[input.output];
|
86
|
+
continue;
|
87
|
+
}
|
88
|
+
inputs[inputName] = input.map((input2) => {
|
89
|
+
const evaluated = evaluateInstance(input2.instanceId);
|
90
|
+
return evaluated[input2.output];
|
91
|
+
});
|
92
|
+
}
|
93
|
+
const component = library.components[instance.type];
|
94
|
+
if (!component) {
|
95
|
+
throw new Error(`Component not found: ${instance.type}, required by instance: ${instanceId}`);
|
96
|
+
}
|
97
|
+
return component({
|
98
|
+
name: instance.name,
|
99
|
+
args: instance.args,
|
100
|
+
inputs
|
101
|
+
});
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
const data = workerData;
|
106
|
+
const logger = pino({ name: "library-worker", level: data.logLevel });
|
107
|
+
let jiti;
|
108
|
+
let library;
|
109
|
+
try {
|
110
|
+
logger.info("worker started");
|
111
|
+
logger.trace("worker data", data);
|
112
|
+
jiti = createJiti(import.meta.filename);
|
113
|
+
logger.debug("jiti created", { filename: import.meta.filename });
|
114
|
+
library = await loadLibrary(jiti, logger, data.modulePaths);
|
115
|
+
parentPort.postMessage({
|
116
|
+
type: "library",
|
117
|
+
library: {
|
118
|
+
components: mapValues(library.components, (component) => component.model),
|
119
|
+
entities: library.entities
|
120
|
+
}
|
121
|
+
});
|
122
|
+
logger.info("library loaded and sent");
|
123
|
+
} catch (error) {
|
124
|
+
logger.error("failed to load library", error);
|
125
|
+
parentPort.postMessage({
|
126
|
+
type: "error",
|
127
|
+
error: error instanceof Error ? error.stack ?? error.message : error
|
128
|
+
});
|
129
|
+
}
|
130
|
+
parentPort.on("message", async (message) => {
|
131
|
+
try {
|
132
|
+
const messageData = message;
|
133
|
+
if (messageData.type === "evaluate-composite-instances") {
|
134
|
+
logger.info("received evaluation request", {
|
135
|
+
totalInstanceCount: messageData.allInstances.length,
|
136
|
+
requestedInstanceCount: messageData.instanceIds?.length ?? -1
|
137
|
+
});
|
138
|
+
logger.trace("evaluation request data", messageData);
|
139
|
+
const instances = evaluateInstances(
|
140
|
+
logger,
|
141
|
+
library,
|
142
|
+
messageData.allInstances,
|
143
|
+
messageData.instanceIds
|
144
|
+
);
|
145
|
+
logger.info("evaluation completed", { instanceCount: instances.length });
|
146
|
+
parentPort.postMessage({
|
147
|
+
type: "instances",
|
148
|
+
instances
|
149
|
+
});
|
150
|
+
} else {
|
151
|
+
logger.info("received module evaluation request", {
|
152
|
+
moduleCount: messageData.modulePaths.length
|
153
|
+
});
|
154
|
+
logger.trace("module evaluation request data", messageData);
|
155
|
+
const instances = await evaluateModules(jiti, logger, messageData.modulePaths);
|
156
|
+
logger.info("module evaluation completed", { instanceCount: instances.length });
|
157
|
+
parentPort.postMessage({
|
158
|
+
type: "instances",
|
159
|
+
instances
|
160
|
+
});
|
161
|
+
}
|
162
|
+
} catch (error) {
|
163
|
+
logger.error("failed to evaluate", error);
|
164
|
+
parentPort.postMessage({
|
165
|
+
type: "error",
|
166
|
+
error: error instanceof Error ? error.stack ?? error.message : error
|
167
|
+
});
|
168
|
+
}
|
169
|
+
});
|