@highstate/backend 0.9.13 → 0.9.15

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.
@@ -113,16 +113,16 @@ function evaluateInstances(logger2, library2, allInstances, resolvedInputs, inst
113
113
  const results = [];
114
114
  const allInstancesMap = new Map(allInstances.map((instance) => [instance.id, instance]));
115
115
  for (const instanceId of instanceIds ?? []) {
116
- const externalInstanceIds = new Set(allInstances.map((instance) => instance.id));
117
- externalInstanceIds.delete(instanceId);
118
116
  try {
119
117
  logger2.debug({ instanceId }, "evaluating top-level instance");
120
- resetEvaluation(externalInstanceIds);
118
+ resetEvaluation();
121
119
  evaluateInstance(instanceId);
122
120
  results.push({
123
121
  success: true,
124
122
  instanceId,
125
- compositeInstances: getCompositeInstances()
123
+ compositeInstances: getCompositeInstances().filter(
124
+ (instance) => instanceId.includes(instance.instance.id) || !allInstancesMap.has(instance.instance.id)
125
+ )
126
126
  });
127
127
  } catch (error) {
128
128
  results.push({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/library/worker/main.ts","../../../src/library/worker/loader.ts","../../../src/library/worker/evaluator.ts"],"sourcesContent":["import type { WorkerData, WorkerRequest, WorkerResponse } from \"./protocol\"\nimport { parentPort, workerData } from \"node:worker_threads\"\nimport { createJiti, type Jiti } from \"jiti\"\nimport { pino } from \"pino\"\nimport { mapValues } from \"remeda\"\nimport { errorToString } from \"../../common\"\nimport { loadLibrary, type Library } from \"./loader\"\nimport { evaluateInstances, evaluateModules } from \"./evaluator\"\n\nconst data = workerData as WorkerData\n\nconst logger = pino({ name: \"library-worker\", level: data.logLevel })\nlet jiti: Jiti\nlet library: Library\n\ntry {\n logger.info(\"worker started\")\n logger.trace({ data }, \"worker data\")\n\n jiti = createJiti(import.meta.filename)\n logger.debug({ filename: import.meta.filename }, \"jiti created\")\n\n library = await loadLibrary(jiti, logger, data.modulePaths)\n\n parentPort!.postMessage({\n type: \"library\",\n library: {\n components: mapValues(library.components, component => component.model),\n entities: library.entities,\n },\n } satisfies WorkerResponse)\n\n logger.info(\"library loaded and sent\")\n} catch (error) {\n logger.error({ error }, \"failed to load library\")\n\n parentPort!.postMessage({\n type: \"error\",\n error: errorToString(error),\n })\n}\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises\nparentPort!.on(\"message\", async message => {\n try {\n const request = message as WorkerRequest\n\n switch (request.type) {\n case \"evaluate-composite-instances\": {\n const results = evaluateInstances(\n logger,\n library,\n request.allInstances,\n request.resolvedInputs,\n request.instanceIds,\n )\n\n parentPort!.postMessage({\n type: \"instance-evaluation-results\",\n results,\n } satisfies WorkerResponse)\n break\n }\n case \"evaluate-modules\": {\n const result = await evaluateModules(jiti, logger, request.modulePaths)\n\n parentPort!.postMessage({\n type: \"module-evaluation-result\",\n result,\n } satisfies WorkerResponse)\n break\n }\n }\n } catch (error) {\n logger.error({ error }, \"failed to evaluate\")\n\n parentPort!.postMessage({\n type: \"error\",\n error: errorToString(error),\n } satisfies WorkerResponse)\n }\n})\n","import type { Logger } from \"pino\"\nimport type { Jiti } from \"jiti\"\nimport Module from \"node:module\"\nimport {\n type Component,\n type Entity,\n isComponent,\n isEntity,\n isUnitModel,\n originalCreate,\n} from \"@highstate/contract\"\nimport { serializeFunction } from \"@pulumi/pulumi/runtime/index.js\"\nimport { sha256 } from \"crypto-hash\"\n\ndeclare module \"module\" {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Module {\n function _load(request: unknown, parent: unknown, isMain: boolean): unknown\n }\n}\n\nconst originalLoad = Module._load\n\nModule._load = function (request: unknown, parent: unknown, isMain: boolean) {\n if (request === \"trace_events\") {\n return {}\n }\n\n return originalLoad(request, parent, isMain)\n}\n\nexport type Library = Readonly<{\n components: Readonly<Record<string, Component>>\n entities: Readonly<Record<string, Entity>>\n}>\n\nexport async function loadLibrary(\n jiti: Jiti,\n logger: Logger,\n modulePaths: string[],\n): Promise<Library> {\n const modules: Record<string, unknown> = {}\n for (const modulePath of modulePaths) {\n try {\n logger.debug({ modulePath }, \"loading module\")\n modules[modulePath] = await jiti.import(modulePath)\n\n logger.debug({ modulePath }, \"module loaded\")\n } catch (err) {\n logger.error({ modulePath, err }, \"module load failed\")\n }\n }\n\n const components: Record<string, Component> = {}\n const entities: Record<string, Entity> = {}\n\n await _loadLibrary(modules, components, entities)\n\n logger.info(\n {\n componentCount: Object.keys(components).length,\n entityCount: Object.keys(entities).length,\n },\n \"library loaded\",\n )\n\n logger.trace({ components, entities }, \"library content\")\n\n return { components, entities }\n}\n\nasync function _loadLibrary(\n value: unknown,\n components: Record<string, Component>,\n entities: Record<string, Entity>,\n): Promise<void> {\n if (isComponent(value)) {\n components[value.model.type] = value\n value.model.definitionHash = await calculateComponentDefinitionHash(value)\n\n for (const entity of value.entities.values()) {\n entity.definitionHash ??= await calculateEntityDefinitionHash(entity)\n }\n return\n }\n\n if (isEntity(value)) {\n entities[value.type] = value\n entities[value.type].definitionHash ??= await calculateEntityDefinitionHash(value)\n return\n }\n\n if (typeof value !== \"object\" || value === null) {\n return\n }\n\n for (const key in value) {\n await _loadLibrary((value as Record<string, unknown>)[key], components, entities)\n }\n}\n\nasync function calculateComponentDefinitionHash(component: Component): Promise<string> {\n if (isUnitModel(component.model)) {\n return await sha256(JSON.stringify(component.model))\n }\n\n const serializedCreate = await serializeFunction(component[originalCreate])\n\n return await sha256(JSON.stringify(component.model) + serializedCreate.text)\n}\n\nasync function calculateEntityDefinitionHash(entity: Entity): Promise<string> {\n return await sha256(JSON.stringify(entity))\n}\n","import type { Logger } from \"pino\"\nimport type { Library } from \"./loader\"\nimport type { Jiti } from \"jiti\"\nimport type { InstanceEvaluationResult, ModuleEvaluationResult } from \"../abstractions\"\nimport type { ResolvedInstanceInput } from \"../../shared\"\nimport { getCompositeInstances, resetEvaluation, type InstanceModel } from \"@highstate/contract\"\nimport { BetterLock } from \"better-lock\"\nimport { errorToString } from \"../../common\"\n\nconst lock = new BetterLock()\n\nexport function evaluateModules(\n jiti: Jiti,\n logger: Logger,\n modulePaths: string[],\n): Promise<ModuleEvaluationResult> {\n return lock.acquire(async () => {\n resetEvaluation()\n let lastModulePath = \"\"\n\n try {\n for (const modulePath of modulePaths) {\n logger.info(\"loading module: %s\", modulePath)\n\n lastModulePath = modulePath\n await jiti.import(modulePath)\n\n logger.debug(\"module loaded: %s\", modulePath)\n }\n\n return {\n success: true,\n compositeInstances: getCompositeInstances(),\n }\n } catch (error) {\n return {\n success: false,\n modulePath: lastModulePath,\n error: errorToString(error),\n }\n }\n })\n}\n\nexport function evaluateInstances(\n logger: Logger,\n library: Library,\n allInstances: InstanceModel[],\n resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,\n instanceIds: string[],\n): InstanceEvaluationResult[] {\n const results: InstanceEvaluationResult[] = []\n const allInstancesMap = new Map(allInstances.map(instance => [instance.id, instance]))\n\n for (const instanceId of instanceIds ?? []) {\n const externalInstanceIds = new Set(allInstances.map(instance => instance.id))\n externalInstanceIds.delete(instanceId)\n\n try {\n logger.debug({ instanceId }, \"evaluating top-level instance\")\n resetEvaluation(externalInstanceIds)\n\n evaluateInstance(instanceId)\n\n results.push({\n success: true,\n instanceId,\n compositeInstances: getCompositeInstances(),\n })\n } catch (error) {\n results.push({\n success: false,\n instanceId,\n error: errorToString(error),\n })\n }\n }\n\n return results\n\n function evaluateInstance(\n instanceId: string,\n instanceOutputs: Map<string, Record<string, unknown>> = new Map(),\n ): Record<string, unknown> {\n let outputs = instanceOutputs.get(instanceId)\n\n if (!outputs) {\n outputs = _evaluateInstance(instanceId, instanceOutputs)\n instanceOutputs.set(instanceId, outputs)\n }\n\n return outputs\n }\n\n function _evaluateInstance(\n instanceId: string,\n instanceOutputs: Map<string, Record<string, unknown>>,\n ): Record<string, unknown> {\n const inputs: Record<string, unknown> = {}\n const instance = allInstancesMap.get(instanceId)\n\n logger.info(\"evaluating instance\", { instanceId })\n\n if (!instance) {\n throw new Error(`Instance not found: ${instanceId}`)\n }\n\n for (const [inputName, input] of Object.entries(resolvedInputs[instanceId] ?? {})) {\n inputs[inputName] = input.map(input => {\n const evaluated = evaluateInstance(input.input.instanceId, instanceOutputs)\n\n return evaluated[input.input.output]\n })\n }\n\n const component = library.components[instance.type]\n if (!component) {\n throw new Error(`Component not found: ${instance.type}, required by instance: ${instanceId}`)\n }\n\n return component({\n name: instance.name,\n args: instance.args as Record<string, never>,\n inputs: inputs as Record<string, never>,\n })\n }\n}\n"],"mappings":"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","names":["jiti","logger","jiti","logger","library","input"]}
1
+ {"version":3,"sources":["../../../src/library/worker/main.ts","../../../src/library/worker/loader.ts","../../../src/library/worker/evaluator.ts"],"sourcesContent":["import type { WorkerData, WorkerRequest, WorkerResponse } from \"./protocol\"\nimport { parentPort, workerData } from \"node:worker_threads\"\nimport { createJiti, type Jiti } from \"jiti\"\nimport { pino } from \"pino\"\nimport { mapValues } from \"remeda\"\nimport { errorToString } from \"../../common\"\nimport { loadLibrary, type Library } from \"./loader\"\nimport { evaluateInstances, evaluateModules } from \"./evaluator\"\n\nconst data = workerData as WorkerData\n\nconst logger = pino({ name: \"library-worker\", level: data.logLevel })\nlet jiti: Jiti\nlet library: Library\n\ntry {\n logger.info(\"worker started\")\n logger.trace({ data }, \"worker data\")\n\n jiti = createJiti(import.meta.filename)\n logger.debug({ filename: import.meta.filename }, \"jiti created\")\n\n library = await loadLibrary(jiti, logger, data.modulePaths)\n\n parentPort!.postMessage({\n type: \"library\",\n library: {\n components: mapValues(library.components, component => component.model),\n entities: library.entities,\n },\n } satisfies WorkerResponse)\n\n logger.info(\"library loaded and sent\")\n} catch (error) {\n logger.error({ error }, \"failed to load library\")\n\n parentPort!.postMessage({\n type: \"error\",\n error: errorToString(error),\n })\n}\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises\nparentPort!.on(\"message\", async message => {\n try {\n const request = message as WorkerRequest\n\n switch (request.type) {\n case \"evaluate-composite-instances\": {\n const results = evaluateInstances(\n logger,\n library,\n request.allInstances,\n request.resolvedInputs,\n request.instanceIds,\n )\n\n parentPort!.postMessage({\n type: \"instance-evaluation-results\",\n results,\n } satisfies WorkerResponse)\n break\n }\n case \"evaluate-modules\": {\n const result = await evaluateModules(jiti, logger, request.modulePaths)\n\n parentPort!.postMessage({\n type: \"module-evaluation-result\",\n result,\n } satisfies WorkerResponse)\n break\n }\n }\n } catch (error) {\n logger.error({ error }, \"failed to evaluate\")\n\n parentPort!.postMessage({\n type: \"error\",\n error: errorToString(error),\n } satisfies WorkerResponse)\n }\n})\n","import type { Logger } from \"pino\"\nimport type { Jiti } from \"jiti\"\nimport Module from \"node:module\"\nimport {\n type Component,\n type Entity,\n isComponent,\n isEntity,\n isUnitModel,\n originalCreate,\n} from \"@highstate/contract\"\nimport { serializeFunction } from \"@pulumi/pulumi/runtime/index.js\"\nimport { sha256 } from \"crypto-hash\"\n\ndeclare module \"module\" {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Module {\n function _load(request: unknown, parent: unknown, isMain: boolean): unknown\n }\n}\n\nconst originalLoad = Module._load\n\nModule._load = function (request: unknown, parent: unknown, isMain: boolean) {\n if (request === \"trace_events\") {\n return {}\n }\n\n return originalLoad(request, parent, isMain)\n}\n\nexport type Library = Readonly<{\n components: Readonly<Record<string, Component>>\n entities: Readonly<Record<string, Entity>>\n}>\n\nexport async function loadLibrary(\n jiti: Jiti,\n logger: Logger,\n modulePaths: string[],\n): Promise<Library> {\n const modules: Record<string, unknown> = {}\n for (const modulePath of modulePaths) {\n try {\n logger.debug({ modulePath }, \"loading module\")\n modules[modulePath] = await jiti.import(modulePath)\n\n logger.debug({ modulePath }, \"module loaded\")\n } catch (err) {\n logger.error({ modulePath, err }, \"module load failed\")\n }\n }\n\n const components: Record<string, Component> = {}\n const entities: Record<string, Entity> = {}\n\n await _loadLibrary(modules, components, entities)\n\n logger.info(\n {\n componentCount: Object.keys(components).length,\n entityCount: Object.keys(entities).length,\n },\n \"library loaded\",\n )\n\n logger.trace({ components, entities }, \"library content\")\n\n return { components, entities }\n}\n\nasync function _loadLibrary(\n value: unknown,\n components: Record<string, Component>,\n entities: Record<string, Entity>,\n): Promise<void> {\n if (isComponent(value)) {\n components[value.model.type] = value\n value.model.definitionHash = await calculateComponentDefinitionHash(value)\n\n for (const entity of value.entities.values()) {\n entity.definitionHash ??= await calculateEntityDefinitionHash(entity)\n }\n return\n }\n\n if (isEntity(value)) {\n entities[value.type] = value\n entities[value.type].definitionHash ??= await calculateEntityDefinitionHash(value)\n return\n }\n\n if (typeof value !== \"object\" || value === null) {\n return\n }\n\n for (const key in value) {\n await _loadLibrary((value as Record<string, unknown>)[key], components, entities)\n }\n}\n\nasync function calculateComponentDefinitionHash(component: Component): Promise<string> {\n if (isUnitModel(component.model)) {\n return await sha256(JSON.stringify(component.model))\n }\n\n const serializedCreate = await serializeFunction(component[originalCreate])\n\n return await sha256(JSON.stringify(component.model) + serializedCreate.text)\n}\n\nasync function calculateEntityDefinitionHash(entity: Entity): Promise<string> {\n return await sha256(JSON.stringify(entity))\n}\n","import type { Logger } from \"pino\"\nimport type { Library } from \"./loader\"\nimport type { Jiti } from \"jiti\"\nimport type { InstanceEvaluationResult, ModuleEvaluationResult } from \"../abstractions\"\nimport type { ResolvedInstanceInput } from \"../../shared\"\nimport { getCompositeInstances, resetEvaluation, type InstanceModel } from \"@highstate/contract\"\nimport { BetterLock } from \"better-lock\"\nimport { errorToString } from \"../../common\"\n\nconst lock = new BetterLock()\n\nexport function evaluateModules(\n jiti: Jiti,\n logger: Logger,\n modulePaths: string[],\n): Promise<ModuleEvaluationResult> {\n return lock.acquire(async () => {\n resetEvaluation()\n let lastModulePath = \"\"\n\n try {\n for (const modulePath of modulePaths) {\n logger.info(\"loading module: %s\", modulePath)\n\n lastModulePath = modulePath\n await jiti.import(modulePath)\n\n logger.debug(\"module loaded: %s\", modulePath)\n }\n\n return {\n success: true,\n compositeInstances: getCompositeInstances(),\n }\n } catch (error) {\n return {\n success: false,\n modulePath: lastModulePath,\n error: errorToString(error),\n }\n }\n })\n}\n\nexport function evaluateInstances(\n logger: Logger,\n library: Library,\n allInstances: InstanceModel[],\n resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>,\n instanceIds: string[],\n): InstanceEvaluationResult[] {\n const results: InstanceEvaluationResult[] = []\n const allInstancesMap = new Map(allInstances.map(instance => [instance.id, instance]))\n\n for (const instanceId of instanceIds ?? []) {\n try {\n logger.debug({ instanceId }, \"evaluating top-level instance\")\n resetEvaluation()\n\n evaluateInstance(instanceId)\n\n results.push({\n success: true,\n instanceId,\n compositeInstances: getCompositeInstances().filter(\n instance =>\n instanceId.includes(instance.instance.id) || !allInstancesMap.has(instance.instance.id),\n ),\n })\n } catch (error) {\n results.push({\n success: false,\n instanceId,\n error: errorToString(error),\n })\n }\n }\n\n return results\n\n function evaluateInstance(\n instanceId: string,\n instanceOutputs: Map<string, Record<string, unknown>> = new Map(),\n ): Record<string, unknown> {\n let outputs = instanceOutputs.get(instanceId)\n\n if (!outputs) {\n outputs = _evaluateInstance(instanceId, instanceOutputs)\n instanceOutputs.set(instanceId, outputs)\n }\n\n return outputs\n }\n\n function _evaluateInstance(\n instanceId: string,\n instanceOutputs: Map<string, Record<string, unknown>>,\n ): Record<string, unknown> {\n const inputs: Record<string, unknown> = {}\n const instance = allInstancesMap.get(instanceId)\n\n logger.info(\"evaluating instance\", { instanceId })\n\n if (!instance) {\n throw new Error(`Instance not found: ${instanceId}`)\n }\n\n for (const [inputName, input] of Object.entries(resolvedInputs[instanceId] ?? {})) {\n inputs[inputName] = input.map(input => {\n const evaluated = evaluateInstance(input.input.instanceId, instanceOutputs)\n\n return evaluated[input.input.output]\n })\n }\n\n const component = library.components[instance.type]\n if (!component) {\n throw new Error(`Component not found: ${instance.type}, required by instance: ${instanceId}`)\n }\n\n return component({\n name: instance.name,\n args: instance.args as Record<string, never>,\n inputs: inputs as Record<string, never>,\n })\n }\n}\n"],"mappings":"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","names":["jiti","logger","jiti","logger","library","input"]}
@@ -5,7 +5,6 @@ import {
5
5
  ValidationResolver,
6
6
  applyLibraryUpdate,
7
7
  applyPartialInstanceState,
8
- buildDependentInstanceStateMap,
9
8
  compositeInstanceSchema,
10
9
  createAsyncBatcher,
11
10
  createInstanceState,
@@ -47,7 +46,7 @@ import {
47
46
  projectOperationSchema,
48
47
  resolverFactories,
49
48
  terminalSessionSchema
50
- } from "../chunk-C2TJAQAD.js";
49
+ } from "../chunk-KTGKNSKM.js";
51
50
  export {
52
51
  GraphResolver,
53
52
  InputHashResolver,
@@ -55,7 +54,6 @@ export {
55
54
  ValidationResolver,
56
55
  applyLibraryUpdate,
57
56
  applyPartialInstanceState,
58
- buildDependentInstanceStateMap,
59
57
  compositeInstanceSchema,
60
58
  createAsyncBatcher,
61
59
  createInstanceState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highstate/backend",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -26,7 +26,7 @@
26
26
  "build": "highstate build"
27
27
  },
28
28
  "dependencies": {
29
- "@highstate/contract": "^0.9.13",
29
+ "@highstate/contract": "^0.9.15",
30
30
  "@types/node": "^22.10.1",
31
31
  "ajv": "^8.17.1",
32
32
  "better-lock": "^3.2.0",
@@ -65,5 +65,5 @@
65
65
  "rollup": "^4.28.1",
66
66
  "typescript": "^5.7.2"
67
67
  },
68
- "gitHead": "409b125db59464bd2b6afa36cc5c92e114c7312b"
68
+ "gitHead": "f61b9905d4cd50511b03331411f42595403ebc06"
69
69
  }
@@ -53,19 +53,19 @@ export function evaluateInstances(
53
53
  const allInstancesMap = new Map(allInstances.map(instance => [instance.id, instance]))
54
54
 
55
55
  for (const instanceId of instanceIds ?? []) {
56
- const externalInstanceIds = new Set(allInstances.map(instance => instance.id))
57
- externalInstanceIds.delete(instanceId)
58
-
59
56
  try {
60
57
  logger.debug({ instanceId }, "evaluating top-level instance")
61
- resetEvaluation(externalInstanceIds)
58
+ resetEvaluation()
62
59
 
63
60
  evaluateInstance(instanceId)
64
61
 
65
62
  results.push({
66
63
  success: true,
67
64
  instanceId,
68
- compositeInstances: getCompositeInstances(),
65
+ compositeInstances: getCompositeInstances().filter(
66
+ instance =>
67
+ instanceId.includes(instance.instance.id) || !allInstancesMap.has(instance.instance.id),
68
+ ),
69
69
  })
70
70
  } catch (error) {
71
71
  results.push({
@@ -84,6 +84,8 @@ export class OperationManager {
84
84
  deleteUnreachableResources: request.options?.deleteUnreachableResources ?? false,
85
85
  forceDeleteState: request.options?.forceDeleteState ?? false,
86
86
  refresh: request.options?.refresh ?? false,
87
+ allowPartialCompositeInstanceUpdates:
88
+ request.options?.allowPartialCompositeInstanceUpdates ?? false,
87
89
  },
88
90
  error: null,
89
91
  startedAt: Date.now(),
@@ -327,6 +327,16 @@ export class OperationWorkset {
327
327
  }
328
328
  }
329
329
 
330
+ // 4. if "allowPartialCompositeInstanceUpdates" is not set, include all children of the composite instances
331
+ if (!this.operation.options.allowPartialCompositeInstanceUpdates) {
332
+ for (const instanceId of this.instanceIdsToUpdate) {
333
+ const children = this.instanceChildrenMap.get(instanceId) ?? []
334
+ for (const child of children) {
335
+ this.instanceIdsToUpdate.add(child.id)
336
+ }
337
+ }
338
+ }
339
+
330
340
  this.operation.instanceIdsToUpdate = Array.from(this.instanceIdsToUpdate)
331
341
  }
332
342
 
@@ -412,6 +422,16 @@ export class OperationWorkset {
412
422
  }
413
423
  }
414
424
 
425
+ // 4. if "allowPartialCompositeInstanceUpdates" is not set, include all children of the composite instances
426
+ if (!this.operation.options.allowPartialCompositeInstanceUpdates) {
427
+ for (const instanceId of this.instanceIdsToDestroy) {
428
+ const children = this.stateChildIdMap.get(instanceId) ?? []
429
+ for (const childId of children) {
430
+ this.instanceIdsToDestroy.add(childId)
431
+ }
432
+ }
433
+ }
434
+
415
435
  this.operation.instanceIdsToDestroy = Array.from(this.instanceIdsToDestroy)
416
436
  }
417
437
 
@@ -430,6 +430,14 @@ export class RuntimeOperation {
430
430
  const dependents = this.workset.getDependentStates(state.id)
431
431
 
432
432
  for (const dependent of dependents) {
433
+ if (
434
+ !this.operation.options.destroyDependentInstances &&
435
+ !this.operation.instanceIdsToDestroy.includes(dependent.id)
436
+ ) {
437
+ // skip dependents that are not affected by the operation
438
+ continue
439
+ }
440
+
433
441
  dependentPromises.push(this.getInstancePromiseForOperation(dependent.id))
434
442
  }
435
443
 
@@ -13,6 +13,7 @@ import {
13
13
  instanceFileSchema,
14
14
  instancePageSchema,
15
15
  instanceStatusFieldSchema,
16
+ instanceTerminalMetaSchema,
16
17
  instanceTerminalSchema,
17
18
  instanceTriggerSchema,
18
19
  type InstancePageBlock,
@@ -606,9 +607,7 @@ export class LocalRunnerBackend implements RunnerBackend {
606
607
  }
607
608
 
608
609
  if (outputs["$terminals"]) {
609
- const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value)
610
-
611
- patch.terminals = terminals.map(terminal => pick(terminal, ["name", "title", "description"]))
610
+ patch.terminals = z.array(instanceTerminalMetaSchema).parse(outputs["$terminals"].value)
612
611
  } else {
613
612
  patch.terminals = []
614
613
  }
@@ -15,6 +15,7 @@ export const operationOptionsSchema = z.object({
15
15
  * Whether to force update all dependencies of the instances even if they are not changed.
16
16
  *
17
17
  * Only applicable for `update`, `preview`, `recreate`, and `refresh` operations.
18
+ *
18
19
  * By default, `false`.
19
20
  */
20
21
  forceUpdateDependencies: z.boolean().default(false),
@@ -23,6 +24,7 @@ export const operationOptionsSchema = z.object({
23
24
  * Whether to force update all children of the composite instances even if they are not changed.
24
25
  *
25
26
  * Only applicable for `update`, `preview`, `recreate`, and `refresh` operations.
27
+ *
26
28
  * By default, `false`.
27
29
  */
28
30
  forceUpdateChildren: z.boolean().default(false),
@@ -30,7 +32,8 @@ export const operationOptionsSchema = z.object({
30
32
  /**
31
33
  * Whether to destroy all dependents of the instances when destroying them.
32
34
  *
33
- * Only applicable for `destroy`.
35
+ * Only applicable for `destroy` and `recreate` operations.
36
+ *
34
37
  * By default, `true`.
35
38
  */
36
39
  destroyDependentInstances: z.boolean().default(true),
@@ -39,6 +42,7 @@ export const operationOptionsSchema = z.object({
39
42
  * Whether to invoke destroy triggers when destroying the instances.
40
43
  *
41
44
  * Only applicable for `destroy`.
45
+ *
42
46
  * By default, `true`.
43
47
  */
44
48
  invokeDestroyTriggers: z.boolean().default(true),
@@ -59,6 +63,16 @@ export const operationOptionsSchema = z.object({
59
63
  */
60
64
  forceDeleteState: z.boolean().default(false),
61
65
 
66
+ /**
67
+ * Whether to allow partial updates of composite instances when updating, destroying or recreating them.
68
+ *
69
+ * If `true`, the operation will only update/destroy composite instance children that are directly referenced by other affected instances.
70
+ * If `false`, the operation will update/destroy all children at of the composite instances at all levels if at least one of them is referenced by affected instances.
71
+ *
72
+ * By default, `false`.
73
+ */
74
+ allowPartialCompositeInstanceUpdates: z.boolean().default(false),
75
+
62
76
  /**
63
77
  * Whether to refresh the state before running the operation.
64
78
  *
@@ -5,6 +5,7 @@ export type ResolverOutputHandler<TOutput> = (id: string, value: TOutput) => voi
5
5
 
6
6
  export abstract class GraphResolver<TNode, TOutput> {
7
7
  private readonly workset: Set<string> = new Set()
8
+ private readonly dependencyMap: Map<string, string[]> = new Map()
8
9
  private readonly dependentMap: Map<string, Set<string>> = new Map()
9
10
  private readonly outputMap: Map<string, TOutput> = new Map()
10
11
 
@@ -52,6 +53,30 @@ export abstract class GraphResolver<TNode, TOutput> {
52
53
  */
53
54
  protected abstract processNode(node: TNode, logger: Logger): TOutput | Promise<TOutput>
54
55
 
56
+ /**
57
+ * Gets all identifiers of the nodes that depend on the given node directly or indirectly.
58
+ */
59
+ getAllDependents(nodeId: string): string[] {
60
+ const result = new Set<string>()
61
+ const stack: string[] = [nodeId]
62
+
63
+ while (stack.length > 0) {
64
+ const dependents = this.dependentMap.get(stack.pop()!)
65
+ if (!dependents) {
66
+ continue
67
+ }
68
+
69
+ for (const dependentId of dependents) {
70
+ if (!result.has(dependentId)) {
71
+ result.add(dependentId)
72
+ stack.push(dependentId)
73
+ }
74
+ }
75
+ }
76
+
77
+ return Array.from(result)
78
+ }
79
+
55
80
  /**
56
81
  * Invalidates the node and all nodes that depend on it.
57
82
  *
@@ -82,9 +107,6 @@ export abstract class GraphResolver<TNode, TOutput> {
82
107
  stack.push(dependentId)
83
108
  }
84
109
  }
85
-
86
- // clear the dependent map for the node
87
- this.dependentMap.delete(nodeId)
88
110
  }
89
111
  }
90
112
 
@@ -94,9 +116,15 @@ export abstract class GraphResolver<TNode, TOutput> {
94
116
  * The abort signal of the previous operation must be called before calling this method again.
95
117
  */
96
118
  async process(signal?: AbortSignal): Promise<void> {
119
+ type StackItem = {
120
+ nodeId: string
121
+ resolved: boolean
122
+ dependencies: string[]
123
+ }
124
+
97
125
  while (this.workset.size > 0) {
98
126
  const rootNodeId = this.workset.values().next().value!
99
- const stack = [{ nodeId: rootNodeId, resolved: false, dependencies: [] as string[] }]
127
+ const stack: StackItem[] = [{ nodeId: rootNodeId, resolved: false, dependencies: [] }]
100
128
 
101
129
  while (stack.length > 0) {
102
130
  const stackItem = stack[stack.length - 1]
@@ -126,14 +154,6 @@ export abstract class GraphResolver<TNode, TOutput> {
126
154
  continue
127
155
  }
128
156
 
129
- // if (stack.some(item => item.nodeId === depId)) {
130
- // this.logger.warn(
131
- // { depId, nodeId, stack },
132
- // "dependency is already in the stack, looks like a circular dependency, skipping",
133
- // )
134
- // continue
135
- // }
136
-
137
157
  if (!this.outputMap.has(depId)) {
138
158
  stack.push({ nodeId: depId, resolved: false, dependencies: [] })
139
159
  hasUnresolvedDependencies = true
@@ -155,7 +175,19 @@ export abstract class GraphResolver<TNode, TOutput> {
155
175
  return
156
176
  }
157
177
 
158
- // update the dependent map
178
+ // remove all dependent nodes
179
+ const oldDependencies = this.dependencyMap.get(nodeId) ?? []
180
+ for (const depId of oldDependencies) {
181
+ const dependantSet = this.dependentMap.get(depId)
182
+ if (dependantSet) {
183
+ dependantSet.delete(nodeId)
184
+ if (dependantSet.size === 0) {
185
+ this.dependentMap.delete(depId)
186
+ }
187
+ }
188
+ }
189
+
190
+ // add the new dependencies
159
191
  for (const depId of stackItem.dependencies) {
160
192
  let dependantSet = this.dependentMap.get(depId)
161
193
  if (!dependantSet) {
@@ -168,6 +200,7 @@ export abstract class GraphResolver<TNode, TOutput> {
168
200
 
169
201
  this.outputMap.set(nodeId, output)
170
202
  this.outputHandler?.(nodeId, output)
203
+ this.dependencyMap.set(nodeId, stackItem.dependencies)
171
204
 
172
205
  stack.pop()
173
206
  }
@@ -1,21 +1,25 @@
1
1
  import type { Logger } from "pino"
2
2
  import type { GraphResolver, ResolverOutputHandler } from "./graph-resolver"
3
+ import type { InstanceState } from "../state"
4
+ import { StateResolver } from "./state"
3
5
  import { InputResolver, type InputResolverNode, type InputResolverOutput } from "./input"
4
6
  import { InputHashResolver, type InputHashNode, type InputHashOutput } from "./input-hash"
5
7
  import { ValidationResolver, type ValidationNode, type ValidationOutput } from "./validation"
6
8
 
7
- export type GraphResolverType = "InputResolver" | "InputHashResolver" | "ValidationResolver"
9
+ export type GraphResolverType = keyof GraphResolverMap
8
10
 
9
11
  export type GraphResolverMap = {
10
12
  InputResolver: [InputResolverNode, InputResolverOutput]
11
13
  InputHashResolver: [InputHashNode, InputHashOutput]
12
14
  ValidationResolver: [ValidationNode, ValidationOutput]
15
+ StateResolver: [InstanceState, void]
13
16
  }
14
17
 
15
18
  export const resolverFactories = {
16
19
  InputResolver,
17
20
  InputHashResolver,
18
21
  ValidationResolver,
22
+ StateResolver,
19
23
  } as Record<
20
24
  GraphResolverType,
21
25
  new (
@@ -0,0 +1,12 @@
1
+ import type { InstanceState } from "../state"
2
+ import { GraphResolver } from "./graph-resolver"
3
+
4
+ export class StateResolver extends GraphResolver<InstanceState, void> {
5
+ protected getNodeDependencies(node: InstanceState): string[] {
6
+ return node.dependencyIds
7
+ }
8
+
9
+ protected processNode(): void {
10
+ return
11
+ }
12
+ }
@@ -72,6 +72,7 @@ export const instanceTerminalMetaSchema = z.object({
72
72
  name: z.string(),
73
73
  title: z.string(),
74
74
  description: z.string().optional(),
75
+ icon: z.string().optional(),
75
76
  })
76
77
 
77
78
  export const instanceTerminalFileSchema = z.object({
@@ -147,6 +148,7 @@ export const instanceStateUpdateSchema = z
147
148
 
148
149
  export type InstanceStatusFieldValue = z.infer<typeof instanceStatusFieldValueSchema>
149
150
  export type InstanceStatusField = z.infer<typeof instanceStatusFieldSchema>
151
+ export type InstanceTerminalMeta = z.infer<typeof instanceTerminalMetaSchema>
150
152
  export type InstanceTerminal = z.infer<typeof instanceTerminalSchema>
151
153
 
152
154
  export type InstanceStatus = z.infer<typeof instanceStatusSchema>
@@ -212,31 +214,6 @@ export function createInstanceStatePatch(update: InstanceStateUpdate): Partial<I
212
214
  return omit(update, ["secrets", "logLine"])
213
215
  }
214
216
 
215
- /**
216
- * Builds a map where instance id -> instance state ids that directly depend on the instance.
217
- *
218
- * @param instanceStates The instance states to build the map from.
219
- */
220
- export function buildDependentInstanceStateMap(
221
- instanceStates: Map<string, InstanceState>,
222
- ): Map<string, string[]> {
223
- const dependentMap = new Map<string, string[]>()
224
-
225
- for (const state of instanceStates.values()) {
226
- for (const dependency of state.dependencyIds) {
227
- let dependents = dependentMap.get(dependency)
228
- if (!dependents) {
229
- dependents = []
230
- dependentMap.set(dependency, dependents)
231
- }
232
-
233
- dependents.push(state.id)
234
- }
235
- }
236
-
237
- return dependentMap
238
- }
239
-
240
217
  /**
241
218
  * Gets all instance ids that depend on the given instance id (including recursively).
242
219
  * The instance id itself is not included in the result.
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod"
2
2
 
3
3
  export const terminalSessionSchema = z.object({
4
- id: z.string().nanoid(),
4
+ id: z.string().uuid(),
5
5
  projectId: z.string(),
6
6
  instanceId: z.string(),
7
7
  terminalName: z.string(),
8
8
  terminalTitle: z.string(),
9
+ terminalIcon: z.string().optional(),
9
10
  createdAt: z.coerce.date(),
10
11
  finishedAt: z.coerce.date().optional(),
11
12
  })
@@ -326,7 +326,7 @@ export class LocalStateBackend implements StateBackend {
326
326
  async getTerminalSessions(projectId: string, instanceId: string): Promise<TerminalSession[]> {
327
327
  const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
328
328
 
329
- return await this.getAllSublevelItems(sublevel, terminalSessionSchema)
329
+ return await this.getAllSublevelItems(sublevel, terminalSessionSchema, 20, { reverse: true })
330
330
  }
331
331
 
332
332
  async getLastTerminalSession(
@@ -5,7 +5,7 @@ import type { RunnerBackend } from "../runner"
5
5
  import { PassThrough, type Stream, type Writable } from "node:stream"
6
6
  import { EventEmitter, on } from "node:events"
7
7
  import { parseInstanceId } from "@highstate/contract"
8
- import { nanoid } from "nanoid"
8
+ import { uuidv7 } from "uuidv7"
9
9
  import { type InstanceTerminal, type TerminalSession, createAsyncBatcher } from "../shared"
10
10
  import { isAbortErrorLike } from "../common"
11
11
 
@@ -97,11 +97,12 @@ export class TerminalManager {
97
97
  }
98
98
 
99
99
  const terminalSession: TerminalSession = {
100
- id: nanoid(),
100
+ id: uuidv7(),
101
101
  projectId,
102
102
  instanceId,
103
103
  terminalName,
104
104
  terminalTitle: factory.title,
105
+ terminalIcon: factory.icon,
105
106
  createdAt: new Date(),
106
107
  }
107
108
 
@@ -117,10 +118,11 @@ export class TerminalManager {
117
118
  projectId: string,
118
119
  instanceId: string,
119
120
  terminalName: string,
121
+ newSession = false,
120
122
  ): Promise<TerminalSession> {
121
123
  const existingSession = this.existingSessions.get(`${projectId}:${instanceId}.${terminalName}`)
122
124
 
123
- if (existingSession) {
125
+ if (existingSession && !newSession) {
124
126
  return existingSession
125
127
  }
126
128
 
@@ -19,6 +19,8 @@ for key in "\${filesKeys[@]}"; do
19
19
  isBinary=$(jq -r ".files[\\"$key\\"].isBinary // false" <<<"$data")
20
20
  content=$(jq -r ".files[\\"$key\\"].content" <<<"$data")
21
21
  mode=$(jq -r ".files[\\"$key\\"].mode // 0" <<<"$data")
22
+
23
+ mkdir -p "$(dirname "$key")"
22
24
 
23
25
  if [ "$isBinary" = "true" ]; then
24
26
  echo "$content" | base64 -d > "$key"