@highstate/backend 0.9.8 → 0.9.10

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  errorToString
3
- } from "../../chunk-EQ4LMS7B.js";
3
+ } from "../../chunk-WXDYCRTT.js";
4
4
 
5
5
  // src/library/worker/main.ts
6
6
  import { parentPort, workerData } from "node:worker_threads";
@@ -1,16 +1,15 @@
1
1
  import {
2
- CircularDependencyError,
2
+ GraphResolver,
3
+ InputHashResolver,
4
+ InputResolver,
5
+ ValidationResolver,
3
6
  applyLibraryUpdate,
4
7
  applyPartialInstanceState,
5
8
  buildDependentInstanceStateMap,
6
9
  compositeInstanceSchema,
7
- createDefaultGraphResolverBackend,
8
- createInputHashResolver,
9
- createInputResolver,
10
+ createAsyncBatcher,
10
11
  createInstanceState,
11
12
  createInstanceStatePatch,
12
- createValidationResolver,
13
- defineGraphResolver,
14
13
  diffLibraries,
15
14
  getAllDependentInstanceIds,
16
15
  getMatchedInjectionInstanceInputs,
@@ -48,20 +47,19 @@ import {
48
47
  projectOperationSchema,
49
48
  resolverFactories,
50
49
  terminalSessionSchema
51
- } from "../chunk-NMGIUI6X.js";
50
+ } from "../chunk-DQDXRDUA.js";
52
51
  export {
53
- CircularDependencyError,
52
+ GraphResolver,
53
+ InputHashResolver,
54
+ InputResolver,
55
+ ValidationResolver,
54
56
  applyLibraryUpdate,
55
57
  applyPartialInstanceState,
56
58
  buildDependentInstanceStateMap,
57
59
  compositeInstanceSchema,
58
- createDefaultGraphResolverBackend,
59
- createInputHashResolver,
60
- createInputResolver,
60
+ createAsyncBatcher,
61
61
  createInstanceState,
62
62
  createInstanceStatePatch,
63
- createValidationResolver,
64
- defineGraphResolver,
65
63
  diffLibraries,
66
64
  getAllDependentInstanceIds,
67
65
  getMatchedInjectionInstanceInputs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highstate/backend",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
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.8",
29
+ "@highstate/contract": "^0.9.10",
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": "036db4d9937ff30edf15f143482c5702e5b7a7fb"
68
+ "gitHead": "aacf8837fdf40f2bb2f83d4c11b35a358e26ec33"
69
69
  }
@@ -61,77 +61,3 @@ export function errorToString(error: unknown): string {
61
61
 
62
62
  return JSON.stringify(error)
63
63
  }
64
-
65
- export type AsyncBatcherOptions = {
66
- waitMs?: number
67
- maxWaitTimeMs?: number
68
- }
69
-
70
- export function createAsyncBatcher<T>(
71
- fn: (items: T[]) => Promise<void>,
72
- { waitMs = 100, maxWaitTimeMs = 1000 }: AsyncBatcherOptions = {},
73
- ) {
74
- let batch: T[] = []
75
- let activeTimeout: NodeJS.Timeout | null = null
76
- let maxWaitTimeout: NodeJS.Timeout | null = null
77
- let firstCallTimestamp: number | null = null
78
-
79
- async function processBatch() {
80
- if (batch.length === 0) return
81
-
82
- const currentBatch = batch
83
- batch = [] // Reset batch before async call
84
-
85
- await fn(currentBatch)
86
-
87
- // Clear max wait timer since batch has been processed
88
- if (maxWaitTimeout) {
89
- clearTimeout(maxWaitTimeout)
90
- maxWaitTimeout = null
91
- }
92
- firstCallTimestamp = null
93
- }
94
-
95
- function schedule() {
96
- if (activeTimeout) clearTimeout(activeTimeout)
97
- activeTimeout = setTimeout(() => {
98
- activeTimeout = null
99
- void processBatch()
100
- }, waitMs)
101
-
102
- // Ensure batch is executed within maxWaitTimeMs
103
- if (!firstCallTimestamp) {
104
- firstCallTimestamp = Date.now()
105
- maxWaitTimeout = setTimeout(() => {
106
- if (activeTimeout) clearTimeout(activeTimeout)
107
- activeTimeout = null
108
- void processBatch()
109
- }, maxWaitTimeMs)
110
- }
111
- }
112
-
113
- return {
114
- /**
115
- * Add an item to the batch.
116
- */
117
- call(item: T): void {
118
- batch.push(item)
119
- schedule()
120
- },
121
-
122
- /**
123
- * Immediately flush the pending batch (if any).
124
- */
125
- async flush(): Promise<void> {
126
- if (activeTimeout) {
127
- clearTimeout(activeTimeout)
128
- activeTimeout = null
129
- }
130
- if (maxWaitTimeout) {
131
- clearTimeout(maxWaitTimeout)
132
- maxWaitTimeout = null
133
- }
134
- await processBatch()
135
- },
136
- }
137
- }
@@ -42,11 +42,6 @@ export interface LibraryBackend {
42
42
  */
43
43
  watchLibrary(signal?: AbortSignal): AsyncIterable<LibraryUpdate[]>
44
44
 
45
- /**
46
- * Gets the currently loaded resolved unit sources.
47
- */
48
- getLoadedResolvedUnitSources(): Promise<ResolvedUnitSource[]>
49
-
50
45
  /**
51
46
  * Gets the resolved unit sources for the given unit types.
52
47
  *
@@ -9,19 +9,18 @@ import {
9
9
  type InstanceModel,
10
10
  } from "@highstate/contract"
11
11
  import { unique } from "remeda"
12
+ import { BetterLock } from "better-lock"
12
13
  import {
13
14
  applyPartialInstanceState,
14
- createInputHashResolver,
15
- createInputResolver,
16
15
  createInstanceState,
17
16
  createInstanceStatePatch,
18
- type GraphResolver,
19
- type InputHashResolverInput,
20
- type InputHashResolverOutput,
21
- type InputResolverInput,
22
- type InputResolverOutput,
17
+ InputHashResolver,
18
+ InputResolver,
19
+ type InputHashNode,
20
+ type InputResolverNode,
23
21
  type InstanceState,
24
22
  type InstanceStateUpdate,
23
+ type InstanceStatus,
25
24
  type LibraryModel,
26
25
  type ProjectOperation,
27
26
  type ResolvedInstanceInput,
@@ -43,17 +42,12 @@ export class OperationWorkset {
43
42
 
44
43
  private readonly unitSourceHashMap = new Map<string, string>()
45
44
 
46
- private inputResolver!: GraphResolver<InputResolverOutput>
47
- private readonly inputResolverInputs = new Map<string, InputResolverInput>()
48
- private readonly inputResolverPromiseCache = new Map<string, Promise<InputResolverOutput>>()
45
+ private inputResolver!: InputResolver
46
+ private readonly inputResolverNodes = new Map<string, InputResolverNode>()
49
47
 
50
- private inputHashResolver!: GraphResolver<InputHashResolverOutput>
51
- private readonly inputHashResolverInputs = new Map<string, InputHashResolverInput>()
52
-
53
- private readonly inputHashResolverPromiseCache = new Map<
54
- string,
55
- Promise<InputHashResolverOutput>
56
- >()
48
+ private inputHashResolver!: InputHashResolver
49
+ private readonly inputHashNodes = new Map<string, InputHashNode>()
50
+ private readonly inputHashResolverLock = new BetterLock()
57
51
 
58
52
  public readonly resolvedInstanceInputs = new Map<
59
53
  string,
@@ -96,12 +90,12 @@ export class OperationWorkset {
96
90
  return this.instanceIdsToUpdate.has(instanceId)
97
91
  }
98
92
 
99
- public updateState(update: InstanceStateUpdate): InstanceState {
93
+ public updateState(update: InstanceStateUpdate, phase: OperationPhase): InstanceState {
100
94
  const finalState = applyPartialInstanceState(this.stateMap, update)
101
95
  this.stateManager.emitStatePatch(this.operation.projectId, createInstanceStatePatch(update))
102
96
 
103
97
  if (finalState.parentId) {
104
- this.recalculateCompositeInstanceState(finalState.parentId)
98
+ this.recalculateCompositeInstanceState(finalState.parentId, phase)
105
99
  }
106
100
 
107
101
  return finalState
@@ -202,7 +196,7 @@ export class OperationWorkset {
202
196
  .filter((state): state is InstanceState => !!state)
203
197
  }
204
198
 
205
- private recalculateCompositeInstanceState(instanceId: string): void {
199
+ private recalculateCompositeInstanceState(instanceId: string, phase: OperationPhase): void {
206
200
  const state = this.stateMap.get(instanceId) ?? createInstanceState(instanceId)
207
201
  let currentResourceCount = 0
208
202
  let totalResourceCount = 0
@@ -222,6 +216,7 @@ export class OperationWorkset {
222
216
 
223
217
  const updatedState = {
224
218
  ...state,
219
+ status: OperationWorkset.getStatusByOperationPhase(phase),
225
220
  currentResourceCount,
226
221
  totalResourceCount,
227
222
  }
@@ -230,7 +225,7 @@ export class OperationWorkset {
230
225
  this.stateManager.emitStatePatch(this.operation.projectId, updatedState)
231
226
 
232
227
  if (state.parentId) {
233
- this.recalculateCompositeInstanceState(state.parentId)
228
+ this.recalculateCompositeInstanceState(state.parentId, phase)
234
229
  }
235
230
  }
236
231
 
@@ -279,7 +274,7 @@ export class OperationWorkset {
279
274
  }
280
275
 
281
276
  const state = this.stateMap.get(instance.id)
282
- const { inputHash: expectedInputHash } = await this.inputHashResolver(instance.id)
277
+ const { inputHash: expectedInputHash } = this.inputHashResolver.requireOutput(instance.id)
283
278
 
284
279
  if (this.operation.options.forceUpdateDependencies) {
285
280
  this.instanceIdsToUpdate.add(instanceId)
@@ -315,7 +310,7 @@ export class OperationWorkset {
315
310
  }
316
311
 
317
312
  const state = this.stateMap.get(child.id)
318
- const { inputHash: expectedInputHash } = await this.inputHashResolver(child.id)
313
+ const { inputHash: expectedInputHash } = this.inputHashResolver.requireOutput(child.id)
319
314
 
320
315
  if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
321
316
  this.instanceIdsToUpdate.add(child.id)
@@ -431,21 +426,35 @@ export class OperationWorkset {
431
426
  return undefined
432
427
  }
433
428
 
429
+ private static getStatusByOperationPhase(phase: OperationPhase): InstanceStatus {
430
+ switch (phase) {
431
+ case "update":
432
+ return "updating"
433
+ case "destroy":
434
+ return "destroying"
435
+ case "refresh":
436
+ return "refreshing"
437
+ }
438
+ }
439
+
434
440
  public async getUpToDateInputHash(instance: InstanceModel): Promise<string> {
435
- const component = this.library.components[instance.type]
436
-
437
- this.inputHashResolverInputs.set(instance.id, {
438
- instance,
439
- component,
440
- resolvedInputs: this.resolvedInstanceInputs.get(instance.id)!,
441
- state: this.stateMap.get(instance.id),
442
- sourceHash: this.getSourceHashIfApplicable(instance, component),
443
- })
441
+ return await this.inputHashResolverLock.acquire(async () => {
442
+ const component = this.library.components[instance.type]
444
443
 
445
- this.inputHashResolverPromiseCache.delete(instance.id)
444
+ this.inputHashNodes.set(instance.id, {
445
+ instance,
446
+ component,
447
+ resolvedInputs: this.resolvedInstanceInputs.get(instance.id)!,
448
+ state: this.stateMap.get(instance.id),
449
+ sourceHash: this.getSourceHashIfApplicable(instance, component),
450
+ })
446
451
 
447
- const { inputHash } = await this.inputHashResolver(instance.id)
448
- return inputHash
452
+ this.inputHashResolver.invalidate(instance.id)
453
+ await this.inputHashResolver.process()
454
+
455
+ const { inputHash } = this.inputHashResolver.requireOutput(instance.id)
456
+ return inputHash
457
+ })
449
458
  }
450
459
 
451
460
  public getLockInstanceIds(): string[] {
@@ -468,10 +477,6 @@ export class OperationWorkset {
468
477
  stateBackend.getAllInstanceStates(operation.projectId, signal),
469
478
  ])
470
479
 
471
- const unitSources = await libraryBackend.getResolvedUnitSources(
472
- unique(project.instances.map(i => i.type)),
473
- )
474
-
475
480
  const workset = new OperationWorkset(
476
481
  operation,
477
482
  library,
@@ -479,10 +484,6 @@ export class OperationWorkset {
479
484
  logger.child({ operationId: operation.id, service: "OperationWorkset" }),
480
485
  )
481
486
 
482
- for (const unitSource of unitSources) {
483
- workset.unitSourceHashMap.set(unitSource.unitType, unitSource.sourceHash)
484
- }
485
-
486
487
  // prepare instances
487
488
  for (const instance of project.instances) {
488
489
  workset.addInstance(instance)
@@ -510,6 +511,14 @@ export class OperationWorkset {
510
511
  }
511
512
  }
512
513
 
514
+ const unitSources = await libraryBackend.getResolvedUnitSources(
515
+ unique(Array.from(workset.instanceMap.values().map(i => i.type))),
516
+ )
517
+
518
+ for (const unitSource of unitSources) {
519
+ workset.unitSourceHashMap.set(unitSource.unitType, unitSource.sourceHash)
520
+ }
521
+
513
522
  for (const state of states) {
514
523
  if (!workset.instanceMap.has(state.id)) {
515
524
  workset.logger.warn(
@@ -523,7 +532,7 @@ export class OperationWorkset {
523
532
 
524
533
  // prepare input resolver
525
534
  for (const instance of workset.instanceMap.values()) {
526
- workset.inputResolverInputs.set(`instance:${instance.id}`, {
535
+ workset.inputResolverNodes.set(`instance:${instance.id}`, {
527
536
  kind: "instance",
528
537
  instance,
529
538
  component: library.components[instance.type],
@@ -531,19 +540,17 @@ export class OperationWorkset {
531
540
  }
532
541
 
533
542
  for (const hub of project.hubs) {
534
- workset.inputResolverInputs.set(`hub:${hub.id}`, { kind: "hub", hub })
543
+ workset.inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub })
535
544
  }
536
545
 
537
- workset.inputResolver = createInputResolver(
538
- //
539
- workset.inputResolverInputs,
540
- logger,
541
- { promiseCache: workset.inputResolverPromiseCache },
542
- )
546
+ workset.inputResolver = new InputResolver(workset.inputResolverNodes, logger)
547
+ workset.inputResolver.addAllNodesToWorkset()
548
+
549
+ await workset.inputResolver.process()
543
550
 
544
551
  // resolve inputs for all instances and pass outputs to input hash resolver
545
552
  for (const instance of workset.instanceMap.values()) {
546
- const output = await workset.inputResolver(`instance:${instance.id}`)
553
+ const output = workset.inputResolver.requireOutput(`instance:${instance.id}`)
547
554
  if (output.kind !== "instance") {
548
555
  throw new Error("Unexpected output kind")
549
556
  }
@@ -552,7 +559,7 @@ export class OperationWorkset {
552
559
 
553
560
  const component = workset.library.components[instance.type]
554
561
 
555
- workset.inputHashResolverInputs.set(instance.id, {
562
+ workset.inputHashNodes.set(instance.id, {
556
563
  instance,
557
564
  component,
558
565
  resolvedInputs: output.resolvedInputs,
@@ -562,12 +569,10 @@ export class OperationWorkset {
562
569
  }
563
570
 
564
571
  // prepare input hash resolver
565
- workset.inputHashResolver = createInputHashResolver(
566
- //
567
- workset.inputHashResolverInputs,
568
- logger,
569
- { promiseCache: workset.inputHashResolverPromiseCache },
570
- )
572
+ workset.inputHashResolver = new InputHashResolver(workset.inputHashNodes, logger)
573
+ workset.inputHashResolver.addAllNodesToWorkset()
574
+
575
+ await workset.inputHashResolver.process()
571
576
 
572
577
  if (operation.type !== "destroy") {
573
578
  await workset.calculateInstanceIdsToUpdate()
@@ -11,13 +11,12 @@ import { mapValues } from "remeda"
11
11
  import {
12
12
  type InstanceState,
13
13
  type InstanceStateUpdate,
14
- type InstanceStatus,
15
14
  type ProjectOperation,
16
15
  type InstanceTriggerInvocation,
17
16
  createInstanceState,
17
+ createAsyncBatcher,
18
18
  } from "../shared"
19
19
  import {
20
- createAsyncBatcher,
21
20
  errorToString,
22
21
  isAbortError,
23
22
  isAbortErrorLike,
@@ -220,7 +219,7 @@ export class RuntimeOperation {
220
219
  ...state,
221
220
  parentId: instance?.parentId,
222
221
  latestOperationId: this.operation.id,
223
- status: this.getStatusByOperationType(),
222
+ status: "pending",
224
223
  error: null,
225
224
  })
226
225
 
@@ -575,7 +574,7 @@ export class RuntimeOperation {
575
574
  return
576
575
  }
577
576
 
578
- const state = this.workset.updateState(patch)
577
+ const state = this.workset.updateState(patch, this.currentPhase)
579
578
 
580
579
  // do not persist anyting for preview operations
581
580
  if (this.operation.type !== "preview") {
@@ -604,21 +603,6 @@ export class RuntimeOperation {
604
603
  return instancePromise
605
604
  }
606
605
 
607
- private getStatusByOperationType(): InstanceStatus {
608
- switch (this.operation.type) {
609
- case "update":
610
- return "updating"
611
- case "preview":
612
- return "previewing"
613
- case "recreate":
614
- return "updating"
615
- case "destroy":
616
- return "destroying"
617
- case "refresh":
618
- return "refreshing"
619
- }
620
- }
621
-
622
606
  private getInstanceDependencies(instance: InstanceModel): InstanceModel[] {
623
607
  const dependencies: InstanceModel[] = []
624
608
  const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}