@highstate/backend 0.7.2 → 0.7.4

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.
Files changed (74) hide show
  1. package/dist/{index.mjs → index.js} +1254 -915
  2. package/dist/library/source-resolution-worker.js +55 -0
  3. package/dist/library/worker/main.js +216 -0
  4. package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
  5. package/dist/shared/index.js +6 -0
  6. package/dist/utils-ByadNcv4.js +102 -0
  7. package/package.json +15 -19
  8. package/src/common/index.ts +3 -0
  9. package/src/common/local.ts +22 -0
  10. package/src/common/pulumi.ts +230 -0
  11. package/src/common/utils.ts +137 -0
  12. package/src/config.ts +40 -0
  13. package/src/index.ts +6 -0
  14. package/src/library/abstractions.ts +83 -0
  15. package/src/library/factory.ts +20 -0
  16. package/src/library/index.ts +2 -0
  17. package/src/library/local.ts +404 -0
  18. package/src/library/source-resolution-worker.ts +96 -0
  19. package/src/library/worker/evaluator.ts +119 -0
  20. package/src/library/worker/loader.ts +110 -0
  21. package/src/library/worker/main.ts +82 -0
  22. package/src/library/worker/protocol.ts +38 -0
  23. package/src/orchestrator/index.ts +1 -0
  24. package/src/orchestrator/manager.ts +165 -0
  25. package/src/orchestrator/operation-workset.ts +483 -0
  26. package/src/orchestrator/operation.ts +647 -0
  27. package/src/preferences/shared.ts +1 -0
  28. package/src/project/abstractions.ts +89 -0
  29. package/src/project/factory.ts +11 -0
  30. package/src/project/index.ts +4 -0
  31. package/src/project/local.ts +412 -0
  32. package/src/project/lock.ts +39 -0
  33. package/src/project/manager.ts +374 -0
  34. package/src/runner/abstractions.ts +146 -0
  35. package/src/runner/factory.ts +22 -0
  36. package/src/runner/index.ts +2 -0
  37. package/src/runner/local.ts +698 -0
  38. package/src/secret/abstractions.ts +59 -0
  39. package/src/secret/factory.ts +22 -0
  40. package/src/secret/index.ts +2 -0
  41. package/src/secret/local.ts +152 -0
  42. package/src/services.ts +133 -0
  43. package/src/shared/index.ts +10 -0
  44. package/src/shared/library.ts +77 -0
  45. package/src/shared/operation.ts +85 -0
  46. package/src/shared/project.ts +62 -0
  47. package/src/shared/resolvers/graph-resolver.ts +111 -0
  48. package/src/shared/resolvers/input-hash.ts +77 -0
  49. package/src/shared/resolvers/input.ts +314 -0
  50. package/src/shared/resolvers/registry.ts +10 -0
  51. package/src/shared/resolvers/validation.ts +94 -0
  52. package/src/shared/state.ts +262 -0
  53. package/src/shared/terminal.ts +13 -0
  54. package/src/state/abstractions.ts +222 -0
  55. package/src/state/factory.ts +22 -0
  56. package/src/state/index.ts +3 -0
  57. package/src/state/local.ts +605 -0
  58. package/src/state/manager.ts +33 -0
  59. package/src/terminal/docker.ts +90 -0
  60. package/src/terminal/factory.ts +20 -0
  61. package/src/terminal/index.ts +3 -0
  62. package/src/terminal/manager.ts +330 -0
  63. package/src/terminal/run.sh.ts +37 -0
  64. package/src/terminal/shared.ts +50 -0
  65. package/src/workspace/abstractions.ts +41 -0
  66. package/src/workspace/factory.ts +14 -0
  67. package/src/workspace/index.ts +2 -0
  68. package/src/workspace/local.ts +54 -0
  69. package/dist/index.d.ts +0 -760
  70. package/dist/library/worker/main.mjs +0 -164
  71. package/dist/runner/source-resolution-worker.mjs +0 -22
  72. package/dist/shared/index.d.ts +0 -85
  73. package/dist/shared/index.mjs +0 -54
  74. package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -1,25 +1,26 @@
1
1
  import { z } from 'zod';
2
2
  import { pickBy, mapKeys, mapValues, omit, pick } from 'remeda';
3
+ import { r as runWithRetryOnError, A as AbortError, s as stringArrayType, c as createAsyncBatcher, i as isAbortErrorLike, e as errorToString, a as isAbortError, t as tryWrapAbortErrorLike } from './utils-ByadNcv4.js';
3
4
  import { BetterLock } from 'better-lock';
4
- import { basename, relative, dirname, resolve as resolve$1 } from 'node:path';
5
- import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
5
+ import { basename, relative, resolve as resolve$1, dirname } from 'node:path';
6
+ import { findWorkspaceDir, readPackageJSON, resolvePackageJSON } from 'pkg-types';
6
7
  import { fileURLToPath } from 'node:url';
7
- import { EventEmitter, on } from 'node:events';
8
+ import EventEmitter$1, { EventEmitter, on } from 'node:events';
8
9
  import { Worker } from 'node:worker_threads';
10
+ import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
11
+ import { isUnitModel, getInstanceId, parseInstanceId } from '@highstate/contract';
9
12
  import Watcher from 'watcher';
10
13
  import { resolve } from 'import-meta-resolve';
11
- import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
12
- import { getInstanceId, isUnitModel, parseInstanceId } from '@highstate/contract';
13
- import { h as hubModelSchema, i as instanceModelSchema, c as createInputResolver, a as createInputHashResolver, b as createInstanceState, d as instanceTerminalSchema, e as instancePageSchema, f as instanceFileSchema, g as instanceStatusFieldSchema, j as instanceTriggerSchema, k as compositeInstanceSchema, p as projectOperationSchema, l as instanceStateSchema, m as isFinalOperationStatus, t as terminalSessionSchema, n as createInstanceStateFrontendPatch, o as applyPartialInstanceState } from './terminal-CqIsctlZ.mjs';
14
- import { sha256 } from 'crypto-hash';
15
- import 'ajv';
14
+ import { U as diffLibraries, e as hubModelSchema, b as instanceModelSchema, x as createInstanceState, M as createInputResolver, R as createInputHashResolver, r as instanceTerminalSchema, n as instancePageSchema, k as instanceFileSchema, g as instanceStatusFieldSchema, t as instanceTriggerSchema, G as projectOperationSchema, v as instanceStateSchema, H as isFinalOperationStatus, c as compositeInstanceSchema, T as terminalSessionSchema, y as applyPartialInstanceState, z as createInstanceStatePatch } from './library-BW5oPM7V.js';
16
15
  import { Readable, PassThrough } from 'node:stream';
17
16
  import { tmpdir, homedir } from 'node:os';
18
17
  import spawn from 'nano-spawn';
19
- import { randomUUID } from 'node:crypto';
20
18
  import { ensureDependencyInstalled } from 'nypm';
19
+ import { sha256 } from 'crypto-hash';
21
20
  import { uuidv7 } from 'uuidv7';
21
+ import { webcrypto } from 'node:crypto';
22
22
  import { pino } from 'pino';
23
+ import 'ajv';
23
24
 
24
25
  class SecretAccessDeniedError extends Error {
25
26
  constructor(projectId, key) {
@@ -27,92 +28,6 @@ class SecretAccessDeniedError extends Error {
27
28
  }
28
29
  }
29
30
 
30
- async function runWithRetryOnError(runner, tryHandleError, maxRetries = 1) {
31
- let lastError;
32
- for (let i = 0; i < maxRetries + 1; i++) {
33
- try {
34
- return await runner();
35
- } catch (e) {
36
- lastError = e;
37
- if (await tryHandleError(e)) {
38
- continue;
39
- }
40
- throw e;
41
- }
42
- }
43
- throw lastError;
44
- }
45
- class AbortError extends Error {
46
- constructor() {
47
- super("Operation aborted");
48
- }
49
- }
50
- function isAbortError(error) {
51
- return error instanceof Error && error.name === "AbortError";
52
- }
53
- const stringArrayType = z.string().transform((args) => args.split(",").map((arg) => arg.trim()));
54
- function errorToString(error) {
55
- if (error instanceof Error) {
56
- return error.stack || error.message;
57
- }
58
- return JSON.stringify(error);
59
- }
60
- function createAsyncBatcher(fn, { waitMs = 100, maxWaitTimeMs = 1e3 } = {}) {
61
- let batch = [];
62
- let activeTimeout = null;
63
- let maxWaitTimeout = null;
64
- let firstCallTimestamp = null;
65
- async function processBatch() {
66
- if (batch.length === 0) return;
67
- const currentBatch = batch;
68
- batch = [];
69
- await fn(currentBatch);
70
- if (maxWaitTimeout) {
71
- clearTimeout(maxWaitTimeout);
72
- maxWaitTimeout = null;
73
- }
74
- firstCallTimestamp = null;
75
- }
76
- function schedule() {
77
- if (activeTimeout) clearTimeout(activeTimeout);
78
- activeTimeout = setTimeout(() => {
79
- activeTimeout = null;
80
- void processBatch();
81
- }, waitMs);
82
- if (!firstCallTimestamp) {
83
- firstCallTimestamp = Date.now();
84
- maxWaitTimeout = setTimeout(() => {
85
- if (activeTimeout) clearTimeout(activeTimeout);
86
- activeTimeout = null;
87
- void processBatch();
88
- }, maxWaitTimeMs);
89
- }
90
- }
91
- return {
92
- /**
93
- * Add an item to the batch.
94
- */
95
- call(item) {
96
- batch.push(item);
97
- schedule();
98
- },
99
- /**
100
- * Immediately flush the pending batch (if any).
101
- */
102
- async flush() {
103
- if (activeTimeout) {
104
- clearTimeout(activeTimeout);
105
- activeTimeout = null;
106
- }
107
- if (maxWaitTimeout) {
108
- clearTimeout(maxWaitTimeout);
109
- maxWaitTimeout = null;
110
- }
111
- await processBatch();
112
- }
113
- };
114
- }
115
-
116
31
  class LocalPulumiHost {
117
32
  constructor(logger) {
118
33
  this.logger = logger;
@@ -128,7 +43,7 @@ class LocalPulumiHost {
128
43
  return null;
129
44
  }
130
45
  }
131
- async runEmpty(options, fn) {
46
+ async runEmpty(options, fn, signal) {
132
47
  const { projectId, pulumiProjectName, pulumiStackName, envVars } = options;
133
48
  return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
134
49
  const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
@@ -150,6 +65,7 @@ class LocalPulumiHost {
150
65
  }
151
66
  }
152
67
  );
68
+ signal?.throwIfAborted();
153
69
  try {
154
70
  return await runWithRetryOnError(
155
71
  () => fn(stack),
@@ -163,7 +79,7 @@ class LocalPulumiHost {
163
79
  }
164
80
  });
165
81
  }
166
- async runLocal(options, fn) {
82
+ async runLocal(options, fn, signal) {
167
83
  const { projectId, pulumiProjectName, pulumiStackName, projectPath, stackConfig, envVars } = options;
168
84
  return await this.lock.acquire(`${pulumiProjectName}.${pulumiStackName}`, async () => {
169
85
  const { LocalWorkspace } = await import('@pulumi/pulumi/automation/index.js');
@@ -189,6 +105,7 @@ class LocalPulumiHost {
189
105
  }
190
106
  }
191
107
  );
108
+ signal?.throwIfAborted();
192
109
  try {
193
110
  return await runWithRetryOnError(
194
111
  () => fn(stack),
@@ -295,7 +212,7 @@ class LocalSecretBackend {
295
212
  isLocked(projectId) {
296
213
  return Promise.resolve(!this.pulumiProjectHost.hasPassword(projectId));
297
214
  }
298
- async unlock(projectId, password) {
215
+ async unlock(projectId, password, signal) {
299
216
  this.pulumiProjectHost.setPassword(projectId, password);
300
217
  try {
301
218
  await this.pulumiProjectHost.runLocal(
@@ -308,7 +225,8 @@ class LocalSecretBackend {
308
225
  async (stack) => {
309
226
  this.logger.debug({ projectId }, "checking password");
310
227
  await stack.info(true);
311
- }
228
+ },
229
+ signal
312
230
  );
313
231
  return true;
314
232
  } catch (error) {
@@ -322,7 +240,7 @@ class LocalSecretBackend {
322
240
  throw error;
323
241
  }
324
242
  }
325
- get(projectId, instanceId) {
243
+ get(projectId, instanceId, signal) {
326
244
  return this.pulumiProjectHost.runLocal(
327
245
  {
328
246
  projectId,
@@ -333,14 +251,16 @@ class LocalSecretBackend {
333
251
  async (stack) => {
334
252
  this.logger.debug({ projectId, instanceId }, "getting secrets");
335
253
  const config = await stack.getAllConfig();
254
+ signal?.throwIfAborted();
336
255
  const prefix = this.getPrefix(projectId, instanceId);
337
256
  const secrets = pickBy(config, (_, key) => key.startsWith(prefix));
338
257
  const trimmedSecrets = mapKeys(secrets, (key) => key.slice(prefix.length));
339
258
  return mapValues(trimmedSecrets, (value) => stringToValue(value.value));
340
- }
259
+ },
260
+ signal
341
261
  );
342
262
  }
343
- set(projectId, instanceId, values) {
263
+ set(projectId, instanceId, values, signal) {
344
264
  return this.pulumiProjectHost.runLocal(
345
265
  {
346
266
  projectId,
@@ -358,9 +278,11 @@ class LocalSecretBackend {
358
278
  })
359
279
  );
360
280
  const config = await stack.getAllConfig();
281
+ signal?.throwIfAborted();
361
282
  Object.assign(config, componentSecrets);
362
283
  await stack.setAllConfig(config);
363
- }
284
+ },
285
+ signal
364
286
  );
365
287
  }
366
288
  getPrefix(projectId, instanceId) {
@@ -394,25 +316,41 @@ function createSecretBackend(config, localPulumiHost, logger) {
394
316
  }
395
317
 
396
318
  const localLibraryBackendConfig = z.object({
397
- HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES: stringArrayType.default("@highstate/library")
319
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES: stringArrayType.default("@highstate/library"),
320
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_SOURCE_BASE_PATH: z.string().optional(),
321
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_EXTRA_SOURCE_WATCH_PATHS: stringArrayType.default("")
398
322
  });
399
323
  class LocalLibraryBackend {
400
- constructor(modulePaths, logger) {
324
+ constructor(modulePaths, sourceBasePath, extraSourceWatchPaths, logger) {
401
325
  this.modulePaths = modulePaths;
326
+ this.sourceBasePath = sourceBasePath;
402
327
  this.logger = logger;
403
- this.watcher = new Watcher(modulePaths, { recursive: true, ignoreInitial: true });
404
- this.watcher.on("all", (event, path) => {
328
+ this.libraryWatcher = new Watcher(modulePaths, { recursive: true, ignoreInitial: true });
329
+ this.libraryWatcher.on("all", (event, path) => {
405
330
  const prefixPath = modulePaths.find((modulePath) => path.startsWith(modulePath));
406
331
  this.logger.info({ msg: "library event", event, path: relative(prefixPath, path) });
407
332
  void this.lock.acquire(() => this.updateLibrary());
408
333
  });
334
+ this.sourceWatcher = new Watcher([sourceBasePath, ...extraSourceWatchPaths], {
335
+ recursive: true,
336
+ ignoreInitial: true,
337
+ ignore: /\.git|node_modules/
338
+ });
339
+ this.sourceWatcher.on("all", (_, path) => {
340
+ if (!path.endsWith("highstate.manifest.json")) {
341
+ return;
342
+ }
343
+ void this.updateUnitSourceHashes(path);
344
+ });
409
345
  this.logger.debug({ msg: "initialized", modulePaths });
410
346
  }
411
- watcher;
347
+ libraryWatcher;
348
+ sourceWatcher;
412
349
  lock = new BetterLock();
413
350
  eventEmitter = new EventEmitter();
414
351
  library = null;
415
352
  worker = null;
353
+ resolvedUnitSources = /* @__PURE__ */ new Map();
416
354
  async loadLibrary() {
417
355
  return await this.lock.acquire(async () => {
418
356
  const [library] = await this.getLibrary();
@@ -424,36 +362,130 @@ class LocalLibraryBackend {
424
362
  yield library;
425
363
  }
426
364
  }
427
- async evaluateCompositeInstances(allInstances, instanceIds) {
365
+ async getResolvedUnitSources() {
366
+ return await this.lock.acquire(async () => {
367
+ const [library] = await this.getLibrary();
368
+ if (!this.resolvedUnitSources.size) {
369
+ await this.syncUnitSources(library);
370
+ }
371
+ return Array.from(this.resolvedUnitSources.values());
372
+ });
373
+ }
374
+ async getResolvedUnitSource(unitType) {
375
+ return await this.lock.acquire(async () => {
376
+ const [library] = await this.getLibrary();
377
+ if (!this.resolvedUnitSources.size) {
378
+ await this.syncUnitSources(library);
379
+ }
380
+ return this.resolvedUnitSources.get(unitType) ?? null;
381
+ });
382
+ }
383
+ async *watchResolvedUnitSources(signal) {
384
+ for await (const [resolvedUnitSource] of on(this.eventEmitter, "resolvedUnitSource", {
385
+ signal
386
+ })) {
387
+ yield resolvedUnitSource;
388
+ }
389
+ }
390
+ async syncUnitSources(library) {
391
+ const unitsToResolve = /* @__PURE__ */ new Map();
392
+ for (const component of Object.values(library.components)) {
393
+ if (!isUnitModel(component)) {
394
+ continue;
395
+ }
396
+ const existingResolvedSource = this.resolvedUnitSources.get(component.type);
397
+ const expectedSource = JSON.stringify(component.source);
398
+ if (existingResolvedSource?.serializedSource !== expectedSource) {
399
+ unitsToResolve.set(component.type, component);
400
+ }
401
+ }
402
+ await this.runSourceResolution(unitsToResolve);
403
+ }
404
+ async runSourceResolution(units) {
405
+ const workerPathUrl = resolve(
406
+ `@highstate/backend/source-resolution-worker`,
407
+ import.meta.url
408
+ );
409
+ const workerPath = fileURLToPath(workerPathUrl);
410
+ const worker = new Worker(workerPath, {
411
+ workerData: {
412
+ requests: Array.from(units.values()).map((unit) => ({
413
+ unitType: unit.type,
414
+ source: unit.source
415
+ })),
416
+ sourceBasePath: this.sourceBasePath,
417
+ logLevel: "error"
418
+ }
419
+ });
420
+ for await (const [event] of on(worker, "message")) {
421
+ const eventData = event;
422
+ if (eventData.type !== "result") {
423
+ throw new Error(`Unexpected message type '${eventData.type}', expected 'result'`);
424
+ }
425
+ for (const result of eventData.results) {
426
+ const unit = units.get(result.unitType);
427
+ if (!unit) {
428
+ this.logger.warn("unit not found for resolved source", { unitType: result.unitType });
429
+ continue;
430
+ }
431
+ const resolvedSource = {
432
+ unitType: result.unitType,
433
+ serializedSource: JSON.stringify(unit.source),
434
+ projectPath: result.projectPath,
435
+ packageJsonPath: result.packageJsonPath,
436
+ allowedDependencies: result.allowedDependencies,
437
+ sourceHash: result.sourceHash
438
+ };
439
+ this.resolvedUnitSources.set(result.unitType, resolvedSource);
440
+ this.eventEmitter.emit("resolvedUnitSource", resolvedSource);
441
+ }
442
+ this.logger.info("unit sources synced");
443
+ return;
444
+ }
445
+ throw new Error("Worker ended without sending the result");
446
+ }
447
+ async evaluateCompositeInstances(allInstances, resolvedInputs, instanceIds) {
428
448
  return await this.lock.acquire(async () => {
429
- this.logger.info("evaluating composite instances", { instanceIds });
449
+ this.logger.info("evaluating %d composite instances", instanceIds.length);
430
450
  const [, worker] = await this.getLibrary();
431
- worker.postMessage({ type: "evaluate-composite-instances", allInstances, instanceIds });
451
+ worker.postMessage({
452
+ type: "evaluate-composite-instances",
453
+ allInstances,
454
+ resolvedInputs,
455
+ instanceIds
456
+ });
432
457
  this.logger.debug("evaluation request sent");
433
- return this.getInstances(worker);
458
+ const { results } = await this.getResult(worker, "instance-evaluation-results");
459
+ return results;
434
460
  });
435
461
  }
436
462
  async evaluateModules(modulePaths) {
437
463
  return await this.lock.acquire(async () => {
438
464
  this.logger.info({ msg: "evaluating modules", modulePaths });
439
465
  const [, worker] = await this.getLibrary();
440
- worker.postMessage({ type: "evaluate-modules", modulePaths });
466
+ worker.postMessage({
467
+ type: "evaluate-modules",
468
+ modulePaths
469
+ });
441
470
  this.logger.debug("evaluation request sent");
442
- return this.getInstances(worker);
471
+ const { result } = await this.getResult(worker, "module-evaluation-result");
472
+ return result;
443
473
  });
444
474
  }
445
- async getInstances(worker) {
475
+ async getResult(worker, expectedType) {
446
476
  for await (const [event] of on(worker, "message")) {
447
477
  const eventData = event;
448
478
  if (eventData.type === "error") {
449
479
  throw new Error(`Worker error: ${eventData.error}`);
450
480
  }
451
- if (eventData.type !== "instances") {
452
- throw new Error(`Unexpected message type '${eventData.type}', expected 'instances'`);
481
+ if (eventData.type !== expectedType) {
482
+ throw new Error(
483
+ `Unexpected response message type "${eventData.type}", expected "${expectedType}"`
484
+ );
453
485
  }
454
- return eventData.instances;
486
+ return eventData;
455
487
  }
456
- throw new Error("Worker ended without sending instances");
488
+ throw new Error("Worker ended without sending any response");
457
489
  }
458
490
  async getLibrary() {
459
491
  if (this.library && this.worker) {
@@ -472,9 +504,14 @@ class LocalLibraryBackend {
472
504
  if (eventData.type !== "library") {
473
505
  throw new Error(`Unexpected message type '${eventData.type}', expected 'library'`);
474
506
  }
475
- this.eventEmitter.emit("library", eventData.library);
507
+ const updates = diffLibraries(
508
+ this.library ?? { components: {}, entities: {} },
509
+ eventData.library
510
+ );
511
+ this.eventEmitter.emit("library", updates);
476
512
  this.library = eventData.library;
477
513
  this.logger.info("library reloaded");
514
+ await this.syncUnitSources(eventData.library);
478
515
  return [this.library, this.worker];
479
516
  }
480
517
  throw new Error("Worker ended without sending library model");
@@ -484,7 +521,49 @@ class LocalLibraryBackend {
484
521
  const workerPath = fileURLToPath(workerPathUrl);
485
522
  return new Worker(workerPath, { workerData });
486
523
  }
487
- static create(config, logger) {
524
+ async updateUnitSourceHashes(path) {
525
+ const packageJsonPath = await resolvePackageJSON(path);
526
+ const packageJson = await readPackageJSON(path);
527
+ const library = await this.loadLibrary();
528
+ const manifestPath = resolve$1(dirname(packageJsonPath), "dist", "highstate.manifest.json");
529
+ let manifest;
530
+ try {
531
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
532
+ } catch (error) {
533
+ this.logger.debug(
534
+ { error },
535
+ `failed to read highstate manifest for package "%s"`,
536
+ packageJson.name
537
+ );
538
+ }
539
+ for (const unit of Object.values(library.components)) {
540
+ if (!isUnitModel(unit)) {
541
+ continue;
542
+ }
543
+ if (unit.source.package !== packageJson.name) {
544
+ continue;
545
+ }
546
+ const relativePath = unit.source.path ? `./dist/${unit.source.path}/index.js` : `./dist/index.js`;
547
+ const sourceHash = manifest?.sourceHashes?.[relativePath];
548
+ if (!sourceHash) {
549
+ this.logger.warn(`source hash not found for unit: "%s"`, unit.type);
550
+ continue;
551
+ }
552
+ const resolvedSource = this.resolvedUnitSources.get(unit.type);
553
+ if (!resolvedSource) {
554
+ this.logger.warn(`resolved source not found for unit: "%s"`, unit.type);
555
+ continue;
556
+ }
557
+ const newResolvedSource = {
558
+ ...resolvedSource,
559
+ sourceHash
560
+ };
561
+ this.resolvedUnitSources.set(unit.type, newResolvedSource);
562
+ this.eventEmitter.emit("resolvedUnitSource", newResolvedSource);
563
+ this.logger.info(`updated source hash for unit: "%s"`, unit.type);
564
+ }
565
+ }
566
+ static async create(config, logger) {
488
567
  const modulePaths = [];
489
568
  for (const module of config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES) {
490
569
  const url = resolve(module, import.meta.url);
@@ -494,8 +573,17 @@ class LocalLibraryBackend {
494
573
  }
495
574
  modulePaths.push(path);
496
575
  }
576
+ let sourceBasePath = config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_SOURCE_BASE_PATH;
577
+ const extraSourceWatchPaths = config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_EXTRA_SOURCE_WATCH_PATHS;
578
+ if (!sourceBasePath) {
579
+ const [projectPath] = await resolveMainLocalProject();
580
+ sourceBasePath = resolve$1(projectPath, "units");
581
+ extraSourceWatchPaths.push(projectPath);
582
+ }
497
583
  return new LocalLibraryBackend(
498
584
  modulePaths,
585
+ sourceBasePath,
586
+ extraSourceWatchPaths,
499
587
  logger.child({ backend: "LibraryBackend", service: "LocalLibraryBackend" })
500
588
  );
501
589
  }
@@ -505,10 +593,10 @@ const libraryBackendConfig = z.object({
505
593
  HIGHSTATE_BACKEND_LIBRARY_TYPE: z.enum(["local"]).default("local"),
506
594
  ...localLibraryBackendConfig.shape
507
595
  });
508
- function createLibraryBackend(config, logger) {
596
+ async function createLibraryBackend(config, logger) {
509
597
  switch (config.HIGHSTATE_BACKEND_LIBRARY_TYPE) {
510
598
  case "local": {
511
- return LocalLibraryBackend.create(config, logger);
599
+ return await LocalLibraryBackend.create(config, logger);
512
600
  }
513
601
  }
514
602
  }
@@ -524,7 +612,7 @@ class LocalProjectBackend {
524
612
  constructor(projectsDir) {
525
613
  this.projectsDir = projectsDir;
526
614
  }
527
- async getProjectNames() {
615
+ async getProjectIds() {
528
616
  try {
529
617
  const files = await readdir(this.projectsDir);
530
618
  return files.filter((file) => file.endsWith(".json")).map((file) => file.replace(/\.json$/, ""));
@@ -825,6 +913,14 @@ class ProjectLock {
825
913
  canImmediatelyAcquireLock(instanceId) {
826
914
  return this.lock.canAcquire(`${this.projectId}/${instanceId}`);
827
915
  }
916
+ canImmediatelyAcquireLocks(instanceIds) {
917
+ for (const instanceId of instanceIds) {
918
+ if (!this.canImmediatelyAcquireLock(instanceId)) {
919
+ return false;
920
+ }
921
+ }
922
+ return true;
923
+ }
828
924
  lockInstance(instanceId, fn) {
829
925
  return this.lock.acquire(`${this.projectId}/${instanceId}`, fn);
830
926
  }
@@ -843,52 +939,36 @@ class ProjectLockManager {
843
939
  }
844
940
 
845
941
  class ProjectManager {
846
- constructor(projectBackend, stateBackend, operationManager, library, logger) {
942
+ constructor(projectBackend, stateBackend, libraryBackend, projectLockManager, stateManager, logger) {
847
943
  this.projectBackend = projectBackend;
848
944
  this.stateBackend = stateBackend;
849
- this.operationManager = operationManager;
850
- this.library = library;
945
+ this.libraryBackend = libraryBackend;
946
+ this.projectLockManager = projectLockManager;
947
+ this.stateManager = stateManager;
851
948
  this.logger = logger;
949
+ void this.watchLibraryChanges();
852
950
  }
853
- async getCompositeInstance(projectId, instanceId) {
854
- const { resolveInputHash } = await this.prepareInputHashResolver(projectId, instanceId);
855
- const [compositeInstance, actualInputHash] = await Promise.all([
856
- this.stateBackend.getCompositeInstance(projectId, instanceId),
857
- resolveInputHash(instanceId)
858
- ]);
859
- if (compositeInstance && compositeInstance.inputHash === actualInputHash) {
860
- return compositeInstance;
951
+ compositeInstanceEE = new EventEmitter();
952
+ async *watchCompositeInstances(projectId, signal) {
953
+ for await (const [children] of on(this.compositeInstanceEE, projectId, {
954
+ signal
955
+ })) {
956
+ yield children;
861
957
  }
862
- this.logger.info("re-evaluating instance since input hash has changed", {
863
- projectId,
864
- instanceId
865
- });
866
- const instancePromise = this.waitForCompositeInstance(
867
- projectId,
868
- instanceId,
869
- actualInputHash,
870
- resolveInputHash
871
- );
872
- await this.operationManager.launch({
873
- type: "evaluate",
874
- projectId,
875
- instanceIds: [instanceId]
876
- });
877
- return await instancePromise;
878
958
  }
879
959
  async createInstance(projectId, instance) {
880
960
  const createdInstance = await this.projectBackend.createInstance(projectId, instance);
881
- await this.updateInstanceChildren(projectId, createdInstance);
961
+ await this.updateCompositeInstance(projectId, createdInstance);
882
962
  return createdInstance;
883
963
  }
884
964
  async updateInstance(projectId, instanceId, patch) {
885
965
  const instance = await this.projectBackend.updateInstance(projectId, instanceId, patch);
886
- await this.updateInstanceChildren(projectId, instance);
966
+ await this.updateCompositeInstance(projectId, instance);
887
967
  return instance;
888
968
  }
889
969
  async renameInstance(projectId, instanceId, newName) {
890
970
  const instance = await this.projectBackend.renameInstance(projectId, instanceId, newName);
891
- await this.updateInstanceChildren(projectId, instance);
971
+ await this.updateCompositeInstance(projectId, instance);
892
972
  return instance;
893
973
  }
894
974
  async deleteInstance(projectId, instanceId) {
@@ -897,11 +977,8 @@ class ProjectManager {
897
977
  this.stateBackend.clearCompositeInstances(projectId, [instanceId])
898
978
  ]);
899
979
  }
900
- async updateInstanceChildren(projectId, instance) {
901
- const { resolveInputHash, library } = await this.prepareInputHashResolver(
902
- projectId,
903
- instance.id
904
- );
980
+ async updateCompositeInstance(projectId, instance) {
981
+ const { resolveInputHash, library } = await this.prepareInputHashResolver(projectId);
905
982
  const component = library.components[instance.type];
906
983
  if (!component) {
907
984
  return;
@@ -909,87 +986,205 @@ class ProjectManager {
909
986
  if (isUnitModel(component)) {
910
987
  return;
911
988
  }
912
- const expectedInputHash = await resolveInputHash(instance.id);
989
+ const { inputHash: expectedInputHash } = await resolveInputHash(instance.id);
913
990
  const inputHash = await this.stateBackend.getCompositeInstanceInputHash(projectId, instance.id);
914
991
  if (inputHash !== expectedInputHash) {
915
992
  this.logger.info("re-evaluating instance since input hash has changed", {
916
993
  projectId,
917
994
  instanceId: instance.id
918
995
  });
919
- await this.operationManager.launch({
920
- type: "evaluate",
921
- projectId,
922
- instanceIds: [instance.id]
923
- });
996
+ await this.evaluateCompositeInstances(projectId, [instance.id]);
924
997
  }
925
998
  }
926
- async waitForCompositeInstance(projectId, instanceId, expectedInputHash, resolveInputHash) {
927
- for await (const instance of this.operationManager.watchCompositeInstance(
928
- projectId,
929
- instanceId
930
- )) {
931
- const actualInputHash = await resolveInputHash(instanceId);
932
- if (actualInputHash !== expectedInputHash) {
933
- throw new Error("Composite instance input hash changed while waiting for evaluation");
999
+ async evaluateCompositeInstances(projectId, instanceIds) {
1000
+ await this.projectLockManager.getLock(projectId).lockInstances(instanceIds, async () => {
1001
+ const [
1002
+ { instances, resolvedInputs, stateMap, resolveInputHash },
1003
+ topLevelCompositeChildrenIds
1004
+ ] = await Promise.all([
1005
+ this.prepareInputHashResolver(projectId),
1006
+ this.stateBackend.getTopLevelCompositeChildrenIds(projectId, instanceIds)
1007
+ ]);
1008
+ const results = await this.libraryBackend.evaluateCompositeInstances(
1009
+ instances,
1010
+ resolvedInputs,
1011
+ instanceIds
1012
+ );
1013
+ const newStates = results.map((result) => {
1014
+ const existingState = stateMap.get(result.instanceId);
1015
+ const newState = existingState ?? createInstanceState(result.instanceId);
1016
+ newState.evaluationError = result.success ? null : result.error;
1017
+ return newState;
1018
+ });
1019
+ const inputHashes = /* @__PURE__ */ new Map();
1020
+ for (const instanceId of instanceIds) {
1021
+ const { inputHash } = await resolveInputHash(instanceId);
1022
+ inputHashes.set(instanceId, inputHash);
1023
+ }
1024
+ const compositeInstances = results.filter((result) => result.success).flatMap(
1025
+ (result) => result.compositeInstances.map((instance) => ({
1026
+ ...instance,
1027
+ inputHash: inputHashes.get(instance.instance.id)
1028
+ }))
1029
+ );
1030
+ const newTopLevelCompositeChildrenIds = Object.fromEntries(
1031
+ results.filter((result) => result.success).map((result) => [
1032
+ result.instanceId,
1033
+ result.compositeInstances.filter((instance) => instance.instance.id !== result.instanceId).map((instance) => instance.instance.id)
1034
+ ])
1035
+ );
1036
+ const deletedCompositeInstanceIds = new Set(
1037
+ Object.values(topLevelCompositeChildrenIds).flat()
1038
+ );
1039
+ for (const childInstanceId of Object.values(newTopLevelCompositeChildrenIds).flat()) {
1040
+ deletedCompositeInstanceIds.delete(childInstanceId);
934
1041
  }
935
- return instance;
936
- }
937
- throw new Error("Composite instance stream ended without receiving the expected instance");
1042
+ await Promise.all([
1043
+ this.stateBackend.clearCompositeInstances(
1044
+ projectId,
1045
+ Array.from(deletedCompositeInstanceIds)
1046
+ ),
1047
+ this.stateBackend.putTopLevelCompositeChildrenIds(
1048
+ projectId,
1049
+ newTopLevelCompositeChildrenIds
1050
+ )
1051
+ ]);
1052
+ for (const state of newStates) {
1053
+ this.stateManager.emitStatePatch(projectId, state);
1054
+ }
1055
+ for (const instance of compositeInstances) {
1056
+ this.compositeInstanceEE.emit(projectId, { type: "updated", instance });
1057
+ }
1058
+ for (const instanceId of deletedCompositeInstanceIds) {
1059
+ this.compositeInstanceEE.emit(projectId, { type: "deleted", instanceId });
1060
+ }
1061
+ const promises = [];
1062
+ if (newStates.length > 0) {
1063
+ promises.push(this.stateBackend.putInstanceStates(projectId, newStates));
1064
+ }
1065
+ if (compositeInstances.length > 0) {
1066
+ promises.push(this.stateBackend.putCompositeInstances(projectId, compositeInstances));
1067
+ }
1068
+ this.logger.info(
1069
+ { projectId },
1070
+ "instance evaluation completed, %d instances persisted",
1071
+ compositeInstances.length
1072
+ );
1073
+ await Promise.all(promises);
1074
+ });
938
1075
  }
939
- async prepareInputHashResolver(projectId, instanceId) {
1076
+ async prepareInputHashResolver(projectId) {
940
1077
  const { instances, hubs } = await this.projectBackend.getProject(projectId);
941
- const library = await this.library.loadLibrary();
942
- const filteredInstances = instances.filter((instance2) => instance2.type in library.components);
943
- const states = await this.stateBackend.getInstanceStates(projectId);
1078
+ const library = await this.libraryBackend.loadLibrary();
1079
+ const filteredInstances = instances.filter((instance) => instance.type in library.components);
1080
+ const states = await this.stateBackend.getAllInstanceStates(projectId);
944
1081
  const stateMap = new Map(states.map((state) => [state.id, state]));
945
- const instance = filteredInstances.find((instance2) => instance2.id === instanceId);
946
- if (!instance) {
947
- throw new Error(`Instance not found: ${instanceId}`);
948
- }
949
1082
  const inputResolverNodes = /* @__PURE__ */ new Map();
950
- for (const instance2 of filteredInstances) {
951
- inputResolverNodes.set(`instance:${instance2.id}`, {
1083
+ for (const instance of filteredInstances) {
1084
+ inputResolverNodes.set(`instance:${instance.id}`, {
952
1085
  kind: "instance",
953
- instance: instance2,
954
- component: library.components[instance2.type]
1086
+ instance,
1087
+ component: library.components[instance.type]
955
1088
  });
956
1089
  }
957
1090
  for (const hub of hubs) {
958
1091
  inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub });
959
1092
  }
960
- const resolveInputs = createInputResolver(
961
- inputResolverNodes,
962
- this.logger.child({ resolver: "input-resolver" })
963
- );
964
- const inputHashNodes = /* @__PURE__ */ new Map();
965
- for (const instance2 of filteredInstances) {
966
- const output = await resolveInputs(`instance:${instance2.id}`);
1093
+ const resolveInputs = createInputResolver(inputResolverNodes, this.logger);
1094
+ const inputHashInputs = /* @__PURE__ */ new Map();
1095
+ const resolvedInputs = {};
1096
+ for (const instance of filteredInstances) {
1097
+ const output = await resolveInputs(`instance:${instance.id}`);
967
1098
  if (output.kind !== "instance") {
968
1099
  throw new Error("Expected instance node");
969
1100
  }
970
- inputHashNodes.set(instance2.id, {
971
- instance: instance2,
1101
+ let sourceHash;
1102
+ if (isUnitModel(library.components[instance.type])) {
1103
+ const resolvedUnit = await this.libraryBackend.getResolvedUnitSource(instance.type);
1104
+ if (!resolvedUnit) {
1105
+ throw new Error(`Resolved unit not found: ${instance.type}`);
1106
+ }
1107
+ sourceHash = resolvedUnit.sourceHash;
1108
+ }
1109
+ inputHashInputs.set(instance.id, {
1110
+ instance,
1111
+ component: library.components[instance.type],
972
1112
  resolvedInputs: output.resolvedInputs,
973
- state: stateMap.get(instance2.id),
974
- sourceHash: void 0
975
- // implement source hash
1113
+ state: stateMap.get(instance.id),
1114
+ sourceHash
976
1115
  });
1116
+ resolvedInputs[instance.id] = output.resolvedInputs;
977
1117
  }
978
- const resolveInputHash = createInputHashResolver(
979
- inputHashNodes,
980
- this.logger.child({ resolver: "input-hash-resolver" })
981
- );
1118
+ const resolveInputHash = createInputHashResolver(inputHashInputs, this.logger);
982
1119
  return {
983
1120
  resolveInputHash,
984
- library
1121
+ library,
1122
+ instances,
1123
+ stateMap,
1124
+ resolvedInputs
985
1125
  };
986
1126
  }
987
- static create(projectBackend, stateBackend, operationManager, library, logger) {
1127
+ async watchLibraryChanges() {
1128
+ for await (const updates of this.libraryBackend.watchLibrary()) {
1129
+ try {
1130
+ await this.handleLibraryUpdates(updates);
1131
+ } catch (error) {
1132
+ this.logger.error({ error }, "failed to handle library updates");
1133
+ }
1134
+ }
1135
+ }
1136
+ async handleLibraryUpdates(updates) {
1137
+ const changedComponents = /* @__PURE__ */ new Set();
1138
+ const library = await this.libraryBackend.loadLibrary();
1139
+ for (const update of updates) {
1140
+ switch (update.type) {
1141
+ case "component-updated":
1142
+ changedComponents.add(update.component.type);
1143
+ break;
1144
+ case "component-removed":
1145
+ changedComponents.add(update.componentType);
1146
+ break;
1147
+ }
1148
+ }
1149
+ if (changedComponents.size === 0) {
1150
+ return;
1151
+ }
1152
+ this.logger.info(
1153
+ { changedComponents },
1154
+ "library components changed, updating composite instances"
1155
+ );
1156
+ const projects = await this.projectBackend.getProjectIds();
1157
+ for (const projectId of projects) {
1158
+ const { resolveInputHash, instances } = await this.prepareInputHashResolver(projectId);
1159
+ const filteredInstances = instances.filter(
1160
+ (instance) => changedComponents.has(instance.type) && library.components[instance.type] && !isUnitModel(library.components[instance.type])
1161
+ );
1162
+ this.logger.info(
1163
+ { projectId, filteredInstanceIds: filteredInstances.map((instance) => instance.id) },
1164
+ "updating composite instances for project"
1165
+ );
1166
+ const inputHashMap = /* @__PURE__ */ new Map();
1167
+ for (const instance of filteredInstances) {
1168
+ const { inputHash } = await resolveInputHash(instance.id);
1169
+ inputHashMap.set(instance.id, inputHash);
1170
+ }
1171
+ try {
1172
+ await this.evaluateCompositeInstances(
1173
+ projectId,
1174
+ filteredInstances.map((instance) => instance.id)
1175
+ );
1176
+ } catch (error) {
1177
+ this.logger.error({ error }, "failed to evaluate composite instances");
1178
+ }
1179
+ }
1180
+ }
1181
+ static create(projectBackend, stateBackend, libraryBackend, projectLockManager, stateManager, logger) {
988
1182
  return new ProjectManager(
989
1183
  projectBackend,
990
1184
  stateBackend,
991
- operationManager,
992
- library,
1185
+ libraryBackend,
1186
+ projectLockManager,
1187
+ stateManager,
993
1188
  logger.child({ service: "ProjectManager" })
994
1189
  );
995
1190
  }
@@ -1002,6 +1197,8 @@ read -r data
1002
1197
  envKeys=($(jq -r '.env | keys[]' <<<"$data"))
1003
1198
  filesKeys=($(jq -r '.files | keys[]' <<<"$data"))
1004
1199
  commandArr=($(jq -r '.command[]' <<<"$data"))
1200
+ cols=$(jq -r '.screenSize.cols' <<<"$data")
1201
+ rows=$(jq -r '.screenSize.rows' <<<"$data")
1005
1202
 
1006
1203
  # Set environment variables
1007
1204
  for key in "\${envKeys[@]}"; do
@@ -1026,20 +1223,24 @@ for key in "\${filesKeys[@]}"; do
1026
1223
  fi
1027
1224
  done
1028
1225
 
1029
- # Execute the command, keeping stdin/stdout open
1030
- exec "\${commandArr[@]}"`;
1226
+ # Execute the command, keeping stdin/stdout open and spawnin a new TTY
1227
+ cmd=$(printf "%q " "\${commandArr[@]}")
1228
+ exec script -q -c "stty cols $cols rows $rows; $cmd" /dev/null
1229
+ `;
1031
1230
 
1032
1231
  const dockerTerminalBackendConfig = z.object({
1033
1232
  HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY: z.string().default("docker"),
1233
+ HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
1034
1234
  HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST: z.string().optional()
1035
1235
  });
1036
1236
  class DockerTerminalBackend {
1037
- constructor(binary, host, logger) {
1237
+ constructor(binary, useSudo, host, logger) {
1038
1238
  this.binary = binary;
1239
+ this.useSudo = useSudo;
1039
1240
  this.host = host;
1040
1241
  this.logger = logger;
1041
1242
  }
1042
- async run({ factory, stdin, stdout, signal }) {
1243
+ async run({ factory, stdin, stdout, screenSize, signal }) {
1043
1244
  const hsTempDir = resolve$1(tmpdir(), "highstate");
1044
1245
  await mkdir(hsTempDir, { recursive: true });
1045
1246
  const runScriptPath = resolve$1(hsTempDir, "run.sh");
@@ -1056,11 +1257,18 @@ class DockerTerminalBackend {
1056
1257
  const initData = {
1057
1258
  command: factory.command,
1058
1259
  cwd: factory.cwd,
1059
- env: factory.env ?? {},
1060
- files: factory.files ?? {}
1260
+ env: {
1261
+ ...factory.env,
1262
+ TERM: "xterm-256color"
1263
+ },
1264
+ files: factory.files ?? {},
1265
+ screenSize
1061
1266
  };
1062
1267
  const initDataStream = Readable.from(JSON.stringify(initData) + "\n");
1063
- const process = spawn(this.binary, args, {
1268
+ if (this.useSudo) {
1269
+ args.unshift(this.binary);
1270
+ }
1271
+ const process = spawn(this.useSudo ? "sudo" : this.binary, args, {
1064
1272
  env: {
1065
1273
  DOCKER_HOST: this.host
1066
1274
  },
@@ -1071,12 +1279,13 @@ class DockerTerminalBackend {
1071
1279
  initDataStream.on("end", () => stdin.pipe(childProcess.stdin));
1072
1280
  childProcess.stdout.pipe(stdout);
1073
1281
  childProcess.stderr.pipe(stdout);
1074
- this.logger.info("process started", { pid: childProcess.pid });
1282
+ this.logger.info({ pid: childProcess.pid }, "process started");
1075
1283
  await process;
1076
1284
  }
1077
1285
  static create(config, logger) {
1078
1286
  return new DockerTerminalBackend(
1079
1287
  config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY,
1288
+ config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO,
1080
1289
  config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST,
1081
1290
  logger.child({ backend: "TerminalBackend", service: "DockerTerminalBackend" })
1082
1291
  );
@@ -1095,39 +1304,95 @@ function createTerminalBackend(config, logger) {
1095
1304
  }
1096
1305
  }
1097
1306
 
1098
- const notAttachedTerminalLifetime = 30 * 1e3;
1307
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1308
+
1309
+ const POOL_SIZE_MULTIPLIER = 128;
1310
+ let pool, poolOffset;
1311
+ function fillPool(bytes) {
1312
+ if (!pool || pool.length < bytes) {
1313
+ pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
1314
+ webcrypto.getRandomValues(pool);
1315
+ poolOffset = 0;
1316
+ } else if (poolOffset + bytes > pool.length) {
1317
+ webcrypto.getRandomValues(pool);
1318
+ poolOffset = 0;
1319
+ }
1320
+ poolOffset += bytes;
1321
+ }
1322
+ function nanoid(size = 21) {
1323
+ fillPool(size |= 0);
1324
+ let id = "";
1325
+ for (let i = poolOffset - size; i < poolOffset; i++) {
1326
+ id += urlAlphabet[pool[i] & 63];
1327
+ }
1328
+ return id;
1329
+ }
1330
+
1331
+ const notAttachedTerminalLifetime = 5 * 60 * 1e3;
1099
1332
  class TerminalManager {
1100
1333
  constructor(terminalBackend, stateBackend, runnerBackend, logger) {
1101
1334
  this.terminalBackend = terminalBackend;
1102
1335
  this.stateBackend = stateBackend;
1103
1336
  this.runnerBackend = runnerBackend;
1104
1337
  this.logger = logger;
1338
+ void this.markFinishedSessions();
1105
1339
  }
1106
1340
  managedTerminals = /* @__PURE__ */ new Map();
1341
+ existingSessions = /* @__PURE__ */ new Map();
1342
+ terminalEE = new EventEmitter();
1343
+ isSessionActive(sessionId) {
1344
+ return this.managedTerminals.has(sessionId);
1345
+ }
1346
+ async *watchSession(projectId, instanceId, sessionId, signal) {
1347
+ const managedTerminal = this.managedTerminals.get(sessionId);
1348
+ if (!managedTerminal) {
1349
+ const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId);
1350
+ if (!session) {
1351
+ throw new Error(`Terminal session "${sessionId}" not found`);
1352
+ }
1353
+ yield session;
1354
+ return;
1355
+ }
1356
+ yield managedTerminal.session;
1357
+ for await (const [terminalSession] of on(this.terminalEE, sessionId, {
1358
+ signal
1359
+ })) {
1360
+ yield terminalSession;
1361
+ if (terminalSession.finishedAt) {
1362
+ return;
1363
+ }
1364
+ }
1365
+ }
1107
1366
  async createSession(projectId, instanceId, terminalName) {
1108
- const terminalSession = {
1109
- id: randomUUID(),
1367
+ const [instanceType, instanceName] = parseInstanceId(instanceId);
1368
+ const factory = await this.runnerBackend.getTerminalFactory(
1369
+ { projectId, instanceType, instanceName },
1110
1370
  terminalName
1371
+ );
1372
+ if (!factory) {
1373
+ throw new Error(
1374
+ `Terminal factory for instance "${instanceId}" with name "${terminalName}" not found`
1375
+ );
1376
+ }
1377
+ const terminalSession = {
1378
+ id: nanoid(),
1379
+ projectId,
1380
+ instanceId,
1381
+ terminalName,
1382
+ terminalTitle: factory.title,
1383
+ createdAt: /* @__PURE__ */ new Date()
1111
1384
  };
1112
1385
  await this.stateBackend.putTerminalSession(projectId, instanceId, terminalSession);
1113
1386
  this.logger.info({ msg: "terminal session created", id: terminalSession.id });
1114
- await this.createManagedTerminal(projectId, instanceId, terminalSession, true);
1387
+ this.createManagedTerminal(factory, terminalSession);
1115
1388
  return terminalSession;
1116
1389
  }
1117
- async ensureSessionCreated(projectId, instanceId, terminalName) {
1118
- const terminalSessions = await this.stateBackend.getTerminalSessions(projectId, instanceId);
1119
- let session = terminalSessions.find((session2) => session2.terminalName === terminalName);
1120
- if (session) {
1121
- this.logger.info({ msg: "reusing existing terminal session", id: session.id });
1122
- } else {
1123
- this.logger.info("creating new terminal session");
1124
- session = await this.createSession(projectId, instanceId, terminalName);
1125
- }
1126
- if (!this.managedTerminals.has(session.id)) {
1127
- this.logger.info({ msg: "no managed terminal found, creating new one", id: session.id });
1128
- await this.createManagedTerminal(projectId, instanceId, session, false);
1390
+ async getOrCreateSession(projectId, instanceId, terminalName) {
1391
+ const existingSession = this.existingSessions.get(`${projectId}:${instanceId}.${terminalName}`);
1392
+ if (existingSession) {
1393
+ return existingSession;
1129
1394
  }
1130
- return session;
1395
+ return await this.createSession(projectId, instanceId, terminalName);
1131
1396
  }
1132
1397
  close(sessionId) {
1133
1398
  this.logger.info({ msg: "closing terminal session", id: sessionId });
@@ -1136,95 +1401,117 @@ class TerminalManager {
1136
1401
  managedTerminal.abortController.abort();
1137
1402
  }
1138
1403
  }
1139
- async attach(projectId, instanceId, sessionId, stdin, stdout, signal) {
1140
- this.logger.info({ msg: "attaching terminal", projectId, instanceId, sessionId });
1141
- let terminal = this.managedTerminals.get(sessionId);
1404
+ attach(sessionId, stdin, stdout, screenSize, signal) {
1405
+ const terminal = this.managedTerminals.get(sessionId);
1142
1406
  if (!terminal) {
1143
- this.logger.info({ msg: "no managed terminal found, creating new one", sessionId });
1144
- const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId);
1145
- if (!session) {
1146
- throw new Error(`Terminal session "${sessionId}" not found`);
1147
- }
1148
- terminal = await this.createManagedTerminal(projectId, instanceId, session, false);
1149
- terminal.history = await this.stateBackend.getTerminalSessionHistory(sessionId);
1150
- }
1151
- if (terminal.attached) {
1152
- throw new Error(`Terminal session "${sessionId}" is already attached`);
1153
- }
1154
- terminal.attached = true;
1155
- for (const line of terminal.history) {
1156
- stdout.write(line);
1407
+ throw new Error(`Terminal session "${sessionId}" not found, check if it's still active`);
1157
1408
  }
1158
- this.logger.info({ msg: "history replayed", count: terminal.history.length, sessionId });
1159
- stdin.pipe(terminal.stdin);
1160
- terminal.stdout.pipe(stdout);
1161
- this.logger.info({ msg: "terminal attached", id: sessionId });
1409
+ const handleStdin = (data) => {
1410
+ terminal.stdin.write(data);
1411
+ };
1412
+ const handleStdout = (data) => {
1413
+ stdout.write(data);
1414
+ };
1415
+ terminal.stdout.on("data", handleStdout);
1416
+ stdin.on("data", handleStdin);
1417
+ terminal.refCount += 1;
1418
+ this.logger.info(
1419
+ "terminal attached (sessionId: %s, refCount: %d)",
1420
+ sessionId,
1421
+ terminal.refCount
1422
+ );
1162
1423
  signal.addEventListener("abort", () => {
1163
- terminal.attached = false;
1164
- this.logger.info({ msg: "terminal detached", id: sessionId });
1424
+ terminal.refCount -= 1;
1425
+ terminal.stdout.off("data", handleStdout);
1426
+ stdin.off("data", handleStdin);
1427
+ this.logger.info(
1428
+ "terminal detached (sessionId: %s, refCount: %d)",
1429
+ sessionId,
1430
+ terminal.refCount
1431
+ );
1165
1432
  });
1433
+ if (!terminal.started) {
1434
+ terminal.start(screenSize);
1435
+ terminal.started = true;
1436
+ }
1166
1437
  }
1167
- async createManagedTerminal(projectId, instanceId, terminalSession, firstLaunch) {
1168
- const [instanceType, instanceName] = parseInstanceId(instanceId);
1169
- const factory = await this.runnerBackend.getTerminalFactory(
1170
- { projectId, instanceType, instanceName },
1171
- terminalSession.terminalName
1438
+ async updateActiveTerminalSessions() {
1439
+ await this.stateBackend.putActiveTerminalSessions(
1440
+ Array.from(this.managedTerminals.values()).map((t) => t.session)
1172
1441
  );
1173
- if (!factory) {
1174
- throw new Error(
1175
- `Terminal factory for instance "${instanceId}" with name "${terminalSession.terminalName}" not found`
1176
- );
1177
- }
1442
+ }
1443
+ createManagedTerminal(factory, terminalSession) {
1178
1444
  const managedTerminal = {
1179
- sessionId: terminalSession.id,
1445
+ session: terminalSession,
1180
1446
  abortController: new AbortController(),
1181
- attached: false,
1182
1447
  stdin: new PassThrough(),
1183
1448
  stdout: new PassThrough(),
1184
- history: []
1449
+ refCount: 0,
1450
+ started: false,
1451
+ start: (screenSize) => {
1452
+ void this.terminalBackend.run({
1453
+ factory,
1454
+ stdin: managedTerminal.stdin,
1455
+ stdout: managedTerminal.stdout,
1456
+ screenSize,
1457
+ signal: managedTerminal.abortController.signal
1458
+ }).catch((error) => {
1459
+ if (isAbortErrorLike(error)) {
1460
+ return;
1461
+ }
1462
+ this.logger.error({
1463
+ msg: "managed terminal failed",
1464
+ id: managedTerminal.session.id,
1465
+ error
1466
+ });
1467
+ }).finally(() => {
1468
+ this.logger.info({ msg: "managed terminal closed", id: managedTerminal.session.id });
1469
+ this.managedTerminals.delete(managedTerminal.session.id);
1470
+ this.existingSessions.delete(
1471
+ `${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`
1472
+ );
1473
+ managedTerminal.session.finishedAt = /* @__PURE__ */ new Date();
1474
+ this.terminalEE.emit(managedTerminal.session.id, managedTerminal.session);
1475
+ void this.stateBackend.putTerminalSession(
1476
+ managedTerminal.session.projectId,
1477
+ managedTerminal.session.instanceId,
1478
+ managedTerminal.session
1479
+ );
1480
+ void this.updateActiveTerminalSessions();
1481
+ });
1482
+ setTimeout(
1483
+ () => this.closeTerminalIfNotAttached(managedTerminal),
1484
+ notAttachedTerminalLifetime
1485
+ );
1486
+ this.logger.info({ msg: "managed terminal created", id: managedTerminal.session.id });
1487
+ }
1185
1488
  };
1186
1489
  managedTerminal.stdout.on("data", (data) => {
1187
- const line = String(data);
1188
- managedTerminal.history.push(String(data));
1189
- void this.persistHistory.call([terminalSession.id, line]);
1190
- });
1191
- this.managedTerminals.set(managedTerminal.sessionId, managedTerminal);
1192
- void this.terminalBackend.run({
1193
- factory: {
1194
- ...factory,
1195
- env: {
1196
- ...factory.env,
1197
- HIGHSTATE_TERMINAL_FIRST_LAUNCH: firstLaunch ? "1" : "0"
1198
- }
1199
- },
1200
- stdin: managedTerminal.stdin,
1201
- stdout: managedTerminal.stdout,
1202
- signal: managedTerminal.abortController.signal
1203
- }).catch((error) => {
1204
- this.logger.error({
1205
- msg: "managed terminal failed",
1206
- id: managedTerminal.sessionId,
1207
- error
1208
- });
1209
- }).finally(() => {
1210
- this.logger.info({ msg: "managed terminal closed", id: managedTerminal.sessionId });
1211
- this.managedTerminals.delete(managedTerminal.sessionId);
1490
+ const entry = String(data);
1491
+ void this.persistHistory.call([terminalSession.id, entry]);
1212
1492
  });
1213
- setTimeout(() => this.closeTerminalIfNotAttached(managedTerminal), notAttachedTerminalLifetime);
1214
- this.logger.info({ msg: "managed terminal created", id: managedTerminal.sessionId });
1493
+ this.managedTerminals.set(managedTerminal.session.id, managedTerminal);
1494
+ this.existingSessions.set(
1495
+ `${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`,
1496
+ managedTerminal.session
1497
+ );
1498
+ void this.updateActiveTerminalSessions();
1215
1499
  return managedTerminal;
1216
1500
  }
1217
1501
  closeTerminalIfNotAttached(terminal) {
1218
- if (!this.managedTerminals.has(terminal.sessionId)) {
1502
+ if (!this.managedTerminals.has(terminal.session.id)) {
1219
1503
  return;
1220
1504
  }
1221
- if (!terminal.attached) {
1505
+ if (terminal.refCount <= 0) {
1222
1506
  this.logger.info({
1223
1507
  msg: "terminal not attached for too long, closing",
1224
- id: terminal.sessionId
1508
+ id: terminal.session.id
1225
1509
  });
1226
1510
  terminal.abortController.abort();
1227
- this.managedTerminals.delete(terminal.sessionId);
1511
+ this.managedTerminals.delete(terminal.session.id);
1512
+ this.existingSessions.delete(
1513
+ `${terminal.session.projectId}:${terminal.session.instanceId}.${terminal.session.terminalName}`
1514
+ );
1228
1515
  return;
1229
1516
  }
1230
1517
  setTimeout(() => this.closeTerminalIfNotAttached(terminal), notAttachedTerminalLifetime);
@@ -1238,9 +1525,19 @@ class TerminalManager {
1238
1525
  );
1239
1526
  }
1240
1527
  persistHistory = createAsyncBatcher(async (entries) => {
1241
- this.logger.trace({ msg: "persisting history lines", count: entries.length });
1528
+ this.logger.trace({ msg: "persisting history entries", count: entries.length });
1242
1529
  await this.stateBackend.appendTerminalSessionHistory(entries);
1243
1530
  });
1531
+ async markFinishedSessions() {
1532
+ const sessions = await this.stateBackend.getActiveTerminalSessions();
1533
+ for (const session of sessions) {
1534
+ await this.stateBackend.putTerminalSession(session.projectId, session.instanceId, {
1535
+ ...session,
1536
+ finishedAt: /* @__PURE__ */ new Date()
1537
+ });
1538
+ }
1539
+ await this.updateActiveTerminalSessions();
1540
+ }
1244
1541
  }
1245
1542
 
1246
1543
  class InvalidInstanceStatusError extends Error {
@@ -1253,20 +1550,17 @@ class InvalidInstanceStatusError extends Error {
1253
1550
  }
1254
1551
 
1255
1552
  const localRunnerBackendConfig = z.object({
1256
- HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_SOURCE_CHECK: z.boolean({ coerce: true }).default(false),
1257
1553
  HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK: z.boolean({ coerce: true }).default(false),
1258
1554
  HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT: z.boolean({ coerce: true }).default(true),
1259
- HIGHSTATE_BACKEND_RUNNER_LOCAL_SOURCE_BASE_PATH: z.string().optional(),
1260
1555
  HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR: z.string().optional()
1261
1556
  });
1262
1557
  class LocalRunnerBackend {
1263
- constructor(skipSourceCheck, skipStateCheck, printOutput, sourceBasePath, cacheDir, pulumiProjectHost) {
1264
- this.skipSourceCheck = skipSourceCheck;
1558
+ constructor(skipStateCheck, printOutput, cacheDir, pulumiProjectHost, libraryBackend) {
1265
1559
  this.skipStateCheck = skipStateCheck;
1266
1560
  this.printOutput = printOutput;
1267
- this.sourceBasePath = sourceBasePath;
1268
1561
  this.cacheDir = cacheDir;
1269
1562
  this.pulumiProjectHost = pulumiProjectHost;
1563
+ this.libraryBackend = libraryBackend;
1270
1564
  }
1271
1565
  events = new EventEmitter();
1272
1566
  async *watch(options) {
@@ -1369,7 +1663,7 @@ class LocalRunnerBackend {
1369
1663
  return null;
1370
1664
  }
1371
1665
  const files = z.array(instanceFileSchema).parse(outputs["$files"].value);
1372
- const file = files.find((f) => f.name === fileName);
1666
+ const file = files.find((f) => f.meta.name === fileName);
1373
1667
  if (!file) {
1374
1668
  return null;
1375
1669
  }
@@ -1412,16 +1706,16 @@ class LocalRunnerBackend {
1412
1706
  async updateWorker(options, configMap, preview) {
1413
1707
  const instanceId = LocalRunnerBackend.getInstanceId(options);
1414
1708
  try {
1415
- const [projectPath, allowedDependencies] = await this.resolveProjectPath(
1416
- options.instanceName,
1417
- options.source
1418
- );
1709
+ const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType);
1710
+ if (!resolvedSource) {
1711
+ throw new Error(`Resolved unit source not found for ${options.instanceType}`);
1712
+ }
1419
1713
  await this.pulumiProjectHost.runLocal(
1420
1714
  {
1421
1715
  projectId: options.projectId,
1422
1716
  pulumiProjectName: options.instanceType,
1423
1717
  pulumiStackName: LocalRunnerBackend.getStackName(options),
1424
- projectPath,
1718
+ projectPath: resolvedSource.projectPath,
1425
1719
  stackConfig: configMap,
1426
1720
  envVars: {
1427
1721
  HIGHSTATE_CACHE_DIR: this.cacheDir
@@ -1482,7 +1776,7 @@ class LocalRunnerBackend {
1482
1776
  if (isUnlocked) return true;
1483
1777
  const isResolved = await this.tryInstallMissingDependencies(
1484
1778
  error,
1485
- allowedDependencies
1779
+ resolvedSource.allowedDependencies
1486
1780
  );
1487
1781
  if (isResolved) return true;
1488
1782
  return false;
@@ -1519,13 +1813,16 @@ class LocalRunnerBackend {
1519
1813
  async destroyWorker(options) {
1520
1814
  const instanceId = LocalRunnerBackend.getInstanceId(options);
1521
1815
  try {
1522
- const [projectPath] = await this.resolveProjectPath(options.instanceName, options.source);
1816
+ const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType);
1817
+ if (!resolvedSource) {
1818
+ throw new Error(`Resolved unit source not found for ${options.instanceType}`);
1819
+ }
1523
1820
  await this.pulumiProjectHost.runLocal(
1524
1821
  {
1525
1822
  projectId: options.projectId,
1526
1823
  pulumiProjectName: options.instanceType,
1527
1824
  pulumiStackName: LocalRunnerBackend.getStackName(options),
1528
- projectPath,
1825
+ projectPath: resolvedSource.projectPath,
1529
1826
  envVars: {
1530
1827
  HIGHSTATE_CACHE_DIR: this.cacheDir,
1531
1828
  PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : ""
@@ -1583,7 +1880,12 @@ class LocalRunnerBackend {
1583
1880
  id: instanceId,
1584
1881
  status: "not_created",
1585
1882
  totalResourceCount: 0,
1586
- currentResourceCount: 0
1883
+ currentResourceCount: 0,
1884
+ statusFields: [],
1885
+ pages: [],
1886
+ files: [],
1887
+ terminals: [],
1888
+ triggers: []
1587
1889
  });
1588
1890
  return;
1589
1891
  }
@@ -1691,7 +1993,7 @@ class LocalRunnerBackend {
1691
1993
  }
1692
1994
  if (outputs["$files"]) {
1693
1995
  const files = z.array(instanceFileSchema).parse(outputs["$files"].value);
1694
- patch.files = files.map((file) => omit(file, ["content"]));
1996
+ patch.files = files.map((file) => file.meta);
1695
1997
  } else {
1696
1998
  patch.files = [];
1697
1999
  }
@@ -1736,35 +2038,9 @@ class LocalRunnerBackend {
1736
2038
  static getInstanceId(options) {
1737
2039
  return getInstanceId(options.instanceType, options.instanceName);
1738
2040
  }
1739
- async resolveProjectPath(instanceType, source) {
1740
- if (source.type === "local") {
1741
- const path = source.path ?? instanceType.replace(/\./g, "/");
1742
- const projectPath = resolve$1(this.sourceBasePath, path);
1743
- return [projectPath, []];
1744
- }
1745
- if (!this.skipSourceCheck) {
1746
- const packageName = source.version ? `${source.package}@${source.version}` : source.package;
1747
- await ensureDependencyInstalled(packageName);
1748
- }
1749
- const workerPathUrl = resolve(
1750
- `@highstate/backend/source-resolution-worker`,
1751
- import.meta.url
1752
- );
1753
- const workerPath = fileURLToPath(workerPathUrl);
1754
- const worker = new Worker(workerPath, {
1755
- workerData: { source, skipSourceCheck: this.skipSourceCheck }
1756
- });
1757
- for await (const [event] of on(worker, "message")) {
1758
- const eventData = event;
1759
- if (eventData.type === "result") {
1760
- return [eventData.projectPath, eventData.allowedDependencies];
1761
- }
1762
- }
1763
- throw new Error("Worker ended without sending the result");
1764
- }
1765
- async tryInstallMissingDependencies(error, allowedDependencies) {
1766
- if (!(error instanceof Error)) {
1767
- return false;
2041
+ async tryInstallMissingDependencies(error, allowedDependencies) {
2042
+ if (!(error instanceof Error)) {
2043
+ return false;
1768
2044
  }
1769
2045
  const pattern = /Cannot find module '(.*)'/;
1770
2046
  const match = error.message.match(pattern);
@@ -1783,12 +2059,7 @@ class LocalRunnerBackend {
1783
2059
  static getStackName(options) {
1784
2060
  return `${options.projectId}_${options.instanceName}`;
1785
2061
  }
1786
- static async create(config, pulumiProjectHost) {
1787
- let sourceBasePath = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SOURCE_BASE_PATH;
1788
- if (!sourceBasePath) {
1789
- const [projectPath] = await resolveMainLocalProject();
1790
- sourceBasePath = resolve$1(projectPath, "units");
1791
- }
2062
+ static create(config, pulumiProjectHost, libraryBackend) {
1792
2063
  let cacheDir = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR;
1793
2064
  if (!cacheDir) {
1794
2065
  const homeDir = process.env.HOME ?? process.env.USERPROFILE;
@@ -1800,12 +2071,11 @@ class LocalRunnerBackend {
1800
2071
  cacheDir = resolve$1(homeDir, ".cache", "highstate");
1801
2072
  }
1802
2073
  return new LocalRunnerBackend(
1803
- config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_SOURCE_CHECK,
1804
2074
  config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK,
1805
2075
  config.HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT,
1806
- sourceBasePath,
1807
2076
  cacheDir,
1808
- pulumiProjectHost
2077
+ pulumiProjectHost,
2078
+ libraryBackend
1809
2079
  );
1810
2080
  }
1811
2081
  }
@@ -1814,18 +2084,14 @@ const runnerBackendConfig = z.object({
1814
2084
  HIGHSTATE_BACKEND_RUNNER_TYPE: z.enum(["local"]).default("local"),
1815
2085
  ...localRunnerBackendConfig.shape
1816
2086
  });
1817
- function createRunnerBackend(config, pulumiProjectHost) {
2087
+ function createRunnerBackend(config, pulumiProjectHost, libraryBackend) {
1818
2088
  switch (config.HIGHSTATE_BACKEND_RUNNER_TYPE) {
1819
2089
  case "local": {
1820
- return LocalRunnerBackend.create(config, pulumiProjectHost);
2090
+ return LocalRunnerBackend.create(config, pulumiProjectHost, libraryBackend);
1821
2091
  }
1822
2092
  }
1823
2093
  }
1824
2094
 
1825
- const evaluatedCompositeInstanceSchema = compositeInstanceSchema.extend({
1826
- inputHash: z.string()
1827
- });
1828
-
1829
2095
  const localStateBackendConfig = z.object({
1830
2096
  HIGHSTATE_BACKEND_STATE_LOCAL_DIR: z.string().optional()
1831
2097
  });
@@ -1837,27 +2103,31 @@ class LocalStateBackend {
1837
2103
  }
1838
2104
  async getActiveOperations() {
1839
2105
  const sublevel = this.getActiveOperationsSublevel();
1840
- return this.getSublevelItems(sublevel, projectOperationSchema);
2106
+ return this.getAllSublevelItems(sublevel, projectOperationSchema);
1841
2107
  }
1842
2108
  async getOperations(projectId, beforeOperationId) {
1843
2109
  const sublevel = this.getProjectOperationsSublevel(projectId);
1844
2110
  const pageSize = 10;
1845
- return await this.getSublevelItems(sublevel, projectOperationSchema, pageSize, {
2111
+ return await this.getAllSublevelItems(sublevel, projectOperationSchema, pageSize, {
1846
2112
  lt: beforeOperationId,
1847
2113
  reverse: true
1848
2114
  });
1849
2115
  }
1850
- async getInstanceStates(projectId) {
2116
+ async getAllInstanceStates(projectId) {
1851
2117
  const sublevel = this.getProjectInstanceStatesSublevel(projectId);
1852
- return await this.getSublevelItems(sublevel, instanceStateSchema);
2118
+ return await this.getAllSublevelItems(sublevel, instanceStateSchema);
1853
2119
  }
1854
2120
  async getInstanceState(projectId, instanceID) {
1855
2121
  const sublevel = this.getProjectInstanceStatesSublevel(projectId);
1856
2122
  return await this.getSublevelItem(sublevel, instanceStateSchema, instanceID);
1857
2123
  }
2124
+ async getInstanceStates(projectId, instanceIds) {
2125
+ const sublevel = this.getProjectInstanceStatesSublevel(projectId);
2126
+ return await this.getSublevelItems(sublevel, instanceStateSchema, instanceIds);
2127
+ }
1858
2128
  async getAffectedInstanceStates(operationId) {
1859
2129
  const sublevel = this.getOperationInstanceStatesSublevel(operationId);
1860
- return await this.getSublevelItems(sublevel, instanceStateSchema);
2130
+ return await this.getAllSublevelItems(sublevel, instanceStateSchema);
1861
2131
  }
1862
2132
  async getInstanceLogs(operationId, instanceId) {
1863
2133
  const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId);
@@ -1889,13 +2159,14 @@ class LocalStateBackend {
1889
2159
  // this separation is also necessary because the instance states can be updated without operations
1890
2160
  states.flatMap((state) => [
1891
2161
  {
2162
+ // always put the state to the operation sublevel for history
1892
2163
  type: "put",
1893
2164
  key: state.id,
1894
2165
  value: state,
1895
2166
  sublevel: operationInstanceStatesSublevel
1896
2167
  },
1897
2168
  {
1898
- type: "put",
2169
+ type: state.status === "not_created" ? "del" : "put",
1899
2170
  key: state.id,
1900
2171
  value: state,
1901
2172
  sublevel: projectInstanceStatesSublevel
@@ -1910,7 +2181,7 @@ class LocalStateBackend {
1910
2181
  // as i told before, we update the instance states without operations
1911
2182
  // this method is used when upstream instance state changes are detected
1912
2183
  states.map((state) => ({
1913
- type: "put",
2184
+ type: state.status === "not_created" ? "del" : "put",
1914
2185
  key: state.id,
1915
2186
  value: state
1916
2187
  }))
@@ -1934,9 +2205,19 @@ class LocalStateBackend {
1934
2205
  }))
1935
2206
  );
1936
2207
  }
2208
+ async getCompositeInstances(projectId, signal) {
2209
+ const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
2210
+ return await this.getAllSublevelItems(
2211
+ //
2212
+ sublevel,
2213
+ compositeInstanceSchema,
2214
+ void 0,
2215
+ { signal }
2216
+ );
2217
+ }
1937
2218
  async getCompositeInstance(projectId, instanceId) {
1938
2219
  const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
1939
- return this.getSublevelItem(sublevel, evaluatedCompositeInstanceSchema, instanceId);
2220
+ return this.getSublevelItem(sublevel, compositeInstanceSchema, instanceId);
1940
2221
  }
1941
2222
  async getCompositeInstanceInputHash(projectId, instanceId) {
1942
2223
  const sublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId);
@@ -1944,22 +2225,16 @@ class LocalStateBackend {
1944
2225
  return inputHash ?? null;
1945
2226
  }
1946
2227
  async putCompositeInstances(projectId, instances) {
1947
- this.validateArray(evaluatedCompositeInstanceSchema, instances);
2228
+ this.validateArray(compositeInstanceSchema, instances);
1948
2229
  const sublevel = this.getProjectCompositeInstancesSublevel(projectId);
1949
- const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId);
2230
+ this.getProjectCompositeInstanceInputHashesSublevel(projectId);
1950
2231
  await this.db.batch(
1951
2232
  instances.flatMap((instance) => [
1952
2233
  {
1953
2234
  type: "put",
1954
2235
  key: instance.instance.id,
1955
- value: evaluatedCompositeInstanceSchema.parse(instance),
2236
+ value: compositeInstanceSchema.parse(instance),
1956
2237
  sublevel
1957
- },
1958
- {
1959
- type: "put",
1960
- key: instance.instance.id,
1961
- value: instance.inputHash,
1962
- sublevel: inputHashesSublevel
1963
2238
  }
1964
2239
  ])
1965
2240
  );
@@ -1974,11 +2249,46 @@ class LocalStateBackend {
1974
2249
  ])
1975
2250
  );
1976
2251
  }
2252
+ async getTopLevelCompositeChildrenIds(projectId, instanceIds) {
2253
+ const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId);
2254
+ const items = await sublevel.getMany(instanceIds);
2255
+ const schema = z.array(z.string()).optional();
2256
+ const result = {};
2257
+ for (let i = 0; i < items.length; i++) {
2258
+ const instanceId = instanceIds[i];
2259
+ const childrenIds = schema.parse(items[i]);
2260
+ result[instanceId] = childrenIds ?? [];
2261
+ }
2262
+ return result;
2263
+ }
2264
+ async putTopLevelCompositeChildrenIds(projectId, childrenIds) {
2265
+ const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId);
2266
+ const schema = z.array(z.string()).optional();
2267
+ await sublevel.batch(
2268
+ Object.entries(childrenIds).map(([instanceId, ids]) => ({
2269
+ type: "put",
2270
+ key: instanceId,
2271
+ value: schema.parse(ids)
2272
+ }))
2273
+ );
2274
+ }
2275
+ async getActiveTerminalSessions() {
2276
+ const data = await this.db.get("activeTerminalSessionIds", { valueEncoding: "json" });
2277
+ return data ? z.array(terminalSessionSchema).parse(data) : [];
2278
+ }
2279
+ putActiveTerminalSessions(sessions) {
2280
+ this.validateArray(terminalSessionSchema, sessions);
2281
+ return this.db.put("activeTerminalSessionIds", sessions, { valueEncoding: "json" });
2282
+ }
2283
+ async getTerminalSession(projectId, instanceId, sessionId) {
2284
+ const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
2285
+ return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId);
2286
+ }
1977
2287
  async getTerminalSessions(projectId, instanceId) {
1978
2288
  const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
1979
- return await this.getSublevelItems(sublevel, terminalSessionSchema);
2289
+ return await this.getAllSublevelItems(sublevel, terminalSessionSchema);
1980
2290
  }
1981
- async getTerminalSession(projectId, instanceId, sessionId) {
2291
+ async getLastTerminalSession(projectId, instanceId, sessionId) {
1982
2292
  const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId);
1983
2293
  return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId);
1984
2294
  }
@@ -2009,7 +2319,7 @@ class LocalStateBackend {
2009
2319
  }))
2010
2320
  );
2011
2321
  }
2012
- async getSublevelItems(sublevel, schema, limit, options) {
2322
+ async getAllSublevelItems(sublevel, schema, limit, options) {
2013
2323
  const result = [];
2014
2324
  const iterator = options ? sublevel.iterator(options) : sublevel.iterator();
2015
2325
  const invalidKeys = [];
@@ -2058,6 +2368,37 @@ class LocalStateBackend {
2058
2368
  }
2059
2369
  return parseResult.data;
2060
2370
  }
2371
+ async getSublevelItems(sublevel, schema, keys) {
2372
+ const result = [];
2373
+ const invalidKeys = [];
2374
+ for (const key of keys) {
2375
+ const value = await sublevel.get(key);
2376
+ if (!value) {
2377
+ continue;
2378
+ }
2379
+ const parseResult = schema.safeParse(value);
2380
+ if (!parseResult.success) {
2381
+ this.logger.warn({
2382
+ msg: "failed to parse item, it will be deleted",
2383
+ error: parseResult.error,
2384
+ sublevel: sublevel.prefix,
2385
+ key
2386
+ });
2387
+ invalidKeys.push(key);
2388
+ continue;
2389
+ }
2390
+ result.push(parseResult.data);
2391
+ }
2392
+ if (invalidKeys.length > 0) {
2393
+ this.logger.info({
2394
+ msg: "deleting invalid items",
2395
+ sublevel: sublevel.prefix,
2396
+ keyCount: invalidKeys.length
2397
+ });
2398
+ await sublevel.batch(invalidKeys.map((key) => ({ type: "del", key })));
2399
+ }
2400
+ return result;
2401
+ }
2061
2402
  validateItem(schema, item) {
2062
2403
  const parseResult = schema.safeParse(item);
2063
2404
  if (!parseResult.success) {
@@ -2088,6 +2429,9 @@ class LocalStateBackend {
2088
2429
  getProjectCompositeInstanceInputHashesSublevel(projectId) {
2089
2430
  return this.getStringSublevel(`projects/${projectId}/compositeInstanceInputHashes`);
2090
2431
  }
2432
+ getProjectCompositeChildrenIdsSublevel(projectId) {
2433
+ return this.getJsonSublevel(`projects/${projectId}/topLevelCompositeChildrenIds`);
2434
+ }
2091
2435
  getProjectInstanceStatesSublevel(projectId) {
2092
2436
  return this.getJsonSublevel(`projects/${projectId}/instanceStates`);
2093
2437
  }
@@ -2150,6 +2494,30 @@ function createStateBackend(config, localPulumiHost, logger) {
2150
2494
  }
2151
2495
  }
2152
2496
 
2497
+ class StateManager {
2498
+ stateEE = new EventEmitter$1();
2499
+ /**
2500
+ * Watches for all instance state changes in the project.
2501
+ *
2502
+ * @param projectId The project ID to watch.
2503
+ * @param signal The signal to abort the operation.
2504
+ */
2505
+ async *watchInstanceStates(projectId, signal) {
2506
+ for await (const [state] of on(this.stateEE, projectId, { signal })) {
2507
+ yield state;
2508
+ }
2509
+ }
2510
+ /**
2511
+ * Emits a state patch for the instance in the project.
2512
+ *
2513
+ * @param projectId The project ID to emit the state patch for.
2514
+ * @param patch The state patch to emit.
2515
+ */
2516
+ emitStatePatch(projectId, patch) {
2517
+ this.stateEE.emit(projectId, patch);
2518
+ }
2519
+ }
2520
+
2153
2521
  const localWorkspaceBackendConfig = z.object({
2154
2522
  HIGHSTATE_BACKEND_WORKSPACE_LOCAL_DIR: z.string().optional()
2155
2523
  });
@@ -2216,8 +2584,352 @@ async function loadConfig(env = process.env, useDotenv = true) {
2216
2584
  return configSchema.parse(env);
2217
2585
  }
2218
2586
 
2587
+ class OperationWorkset {
2588
+ constructor(operation, library, stateManager, logger) {
2589
+ this.operation = operation;
2590
+ this.library = library;
2591
+ this.stateManager = stateManager;
2592
+ this.logger = logger;
2593
+ }
2594
+ affectedInstanceIdSet = /* @__PURE__ */ new Set();
2595
+ instanceMap = /* @__PURE__ */ new Map();
2596
+ instanceChildrenMap = /* @__PURE__ */ new Map();
2597
+ initialStateMap = /* @__PURE__ */ new Map();
2598
+ stateMap = /* @__PURE__ */ new Map();
2599
+ dependentStateMap = /* @__PURE__ */ new Map();
2600
+ stateChildIdMap = /* @__PURE__ */ new Map();
2601
+ unitSourceHashMap = /* @__PURE__ */ new Map();
2602
+ inputResolver;
2603
+ inputResolverInputs = /* @__PURE__ */ new Map();
2604
+ inputResolverPromiseCache = /* @__PURE__ */ new Map();
2605
+ inputHashResolver;
2606
+ inputHashResolverInputs = /* @__PURE__ */ new Map();
2607
+ inputHashResolverPromiseCache = /* @__PURE__ */ new Map();
2608
+ resolvedInstanceInputs = /* @__PURE__ */ new Map();
2609
+ getInstance(instanceId) {
2610
+ const instance = this.instanceMap.get(instanceId);
2611
+ if (!instance) {
2612
+ throw new Error(`Instance with ID ${instanceId} not found in the operation workset`);
2613
+ }
2614
+ return instance;
2615
+ }
2616
+ isAffected(instanceId) {
2617
+ return this.affectedInstanceIdSet.has(instanceId);
2618
+ }
2619
+ updateState(update) {
2620
+ const finalState = applyPartialInstanceState(this.stateMap, update);
2621
+ this.stateManager.emitStatePatch(this.operation.projectId, createInstanceStatePatch(update));
2622
+ if (finalState.parentId) {
2623
+ this.recalculateCompositeState(finalState.parentId);
2624
+ }
2625
+ return finalState;
2626
+ }
2627
+ setState(state) {
2628
+ this.stateMap.set(state.id, state);
2629
+ this.initialStateMap.set(state.id, state);
2630
+ if (state.parentId) {
2631
+ let children = this.stateChildIdMap.get(state.parentId);
2632
+ if (!children) {
2633
+ children = [];
2634
+ this.stateChildIdMap.set(state.parentId, children);
2635
+ }
2636
+ children.push(state.id);
2637
+ }
2638
+ }
2639
+ restoreInitialStatus(instanceId) {
2640
+ const state = this.stateMap.get(instanceId);
2641
+ const initialState = this.initialStateMap.get(instanceId);
2642
+ if (!state || !initialState) {
2643
+ this.logger.warn(
2644
+ `cannot reset status for instance "${instanceId}" because it is not in the state map`
2645
+ );
2646
+ return;
2647
+ }
2648
+ this.stateMap.set(instanceId, { ...initialState, status: initialState.status });
2649
+ }
2650
+ emitAffectedInitialStates() {
2651
+ for (const state of this.initialStateMap.values()) {
2652
+ if (this.affectedInstanceIdSet.has(state.id)) {
2653
+ this.stateManager.emitStatePatch(this.operation.projectId, state);
2654
+ }
2655
+ }
2656
+ }
2657
+ getAffectedCompositeChildren(instanceId) {
2658
+ const children = this.instanceChildrenMap.get(instanceId);
2659
+ if (!children) {
2660
+ return [];
2661
+ }
2662
+ return children.filter((child) => this.affectedInstanceIdSet.has(child.id));
2663
+ }
2664
+ getState(instanceId) {
2665
+ return this.stateMap.get(instanceId);
2666
+ }
2667
+ getDependentStates(instanceId) {
2668
+ return this.dependentStateMap.get(instanceId) ?? [];
2669
+ }
2670
+ recalculateCompositeState(instanceId) {
2671
+ const state = this.stateMap.get(instanceId) ?? createInstanceState(instanceId);
2672
+ let currentResourceCount = 0;
2673
+ let totalResourceCount = 0;
2674
+ const children = this.stateChildIdMap.get(instanceId) ?? [];
2675
+ for (const childId of children) {
2676
+ const child = this.stateMap.get(childId);
2677
+ if (child?.currentResourceCount) {
2678
+ currentResourceCount += child.currentResourceCount;
2679
+ }
2680
+ if (child?.totalResourceCount) {
2681
+ totalResourceCount += child.totalResourceCount;
2682
+ }
2683
+ }
2684
+ const updatedState = {
2685
+ ...state,
2686
+ currentResourceCount,
2687
+ totalResourceCount
2688
+ };
2689
+ this.stateMap.set(instanceId, updatedState);
2690
+ this.stateManager.emitStatePatch(this.operation.projectId, updatedState);
2691
+ if (state.parentId) {
2692
+ this.recalculateCompositeState(state.parentId);
2693
+ }
2694
+ }
2695
+ addInstance(instance) {
2696
+ if (this.instanceMap.has(instance.id)) {
2697
+ throw new Error(`Found multiple instances with the same ID: ${instance.id}`);
2698
+ }
2699
+ if (!(instance.type in this.library.components)) {
2700
+ this.logger.warn(
2701
+ `ignoring instance "${instance.id}" because its type "${instance.type}" is not in the library`
2702
+ );
2703
+ return;
2704
+ }
2705
+ this.instanceMap.set(instance.id, instance);
2706
+ if (instance.parentId) {
2707
+ let children = this.instanceChildrenMap.get(instance.parentId);
2708
+ if (!children) {
2709
+ children = [];
2710
+ this.instanceChildrenMap.set(instance.parentId, children);
2711
+ }
2712
+ children.push(instance);
2713
+ }
2714
+ }
2715
+ async extendForUpdate() {
2716
+ const traverse = async (instanceId) => {
2717
+ if (this.affectedInstanceIdSet.has(instanceId)) {
2718
+ return;
2719
+ }
2720
+ const instance = this.instanceMap.get(instanceId);
2721
+ if (!instance) {
2722
+ return;
2723
+ }
2724
+ const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
2725
+ for (const inputs of Object.values(instanceInputs)) {
2726
+ for (const input of inputs) {
2727
+ await traverse(input.input.instanceId);
2728
+ }
2729
+ }
2730
+ const state = this.stateMap.get(instance.id);
2731
+ const { inputHash: expectedInputHash } = await this.inputHashResolver(instance.id);
2732
+ if (this.operation.options.forceUpdateDependencies) {
2733
+ this.affectedInstanceIdSet.add(instanceId);
2734
+ return;
2735
+ }
2736
+ if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
2737
+ this.affectedInstanceIdSet.add(instanceId);
2738
+ }
2739
+ };
2740
+ for (const instanceId of this.operation.instanceIds) {
2741
+ if (this.operation.type === "update") {
2742
+ await traverse(instanceId);
2743
+ }
2744
+ this.affectedInstanceIdSet.add(instanceId);
2745
+ }
2746
+ const compositeInstanceQueue = Array.from(this.affectedInstanceIdSet);
2747
+ while (compositeInstanceQueue.length > 0) {
2748
+ const childId = compositeInstanceQueue.pop();
2749
+ const children = this.instanceChildrenMap.get(childId) ?? [];
2750
+ for (const child of children) {
2751
+ compositeInstanceQueue.push(child.id);
2752
+ this.affectedInstanceIdSet.add(child.id);
2753
+ }
2754
+ }
2755
+ for (const instanceId of this.affectedInstanceIdSet) {
2756
+ let instance = this.instanceMap.get(instanceId);
2757
+ while (instance?.parentId) {
2758
+ this.affectedInstanceIdSet.add(instance.parentId);
2759
+ instance = this.instanceMap.get(instance.parentId);
2760
+ }
2761
+ }
2762
+ this.operation.affectedInstanceIds = Array.from(this.affectedInstanceIdSet);
2763
+ }
2764
+ extendForDestroy() {
2765
+ const traverse = (instanceKey) => {
2766
+ if (this.affectedInstanceIdSet.has(instanceKey)) {
2767
+ return;
2768
+ }
2769
+ const state = this.stateMap.get(instanceKey);
2770
+ if (!state || state.status === "not_created") {
2771
+ return;
2772
+ }
2773
+ const dependents = this.dependentStateMap.get(instanceKey) ?? [];
2774
+ for (const dependent of dependents) {
2775
+ traverse(dependent.id);
2776
+ this.affectedInstanceIdSet.add(instanceKey);
2777
+ }
2778
+ };
2779
+ for (const instanceId of this.operation.instanceIds) {
2780
+ const instance = this.instanceMap.get(instanceId);
2781
+ if (!instance) {
2782
+ throw new Error(`Instance not found: ${instanceId}`);
2783
+ }
2784
+ if (this.operation.options.destroyDependentInstances) {
2785
+ traverse(instance.id);
2786
+ }
2787
+ this.affectedInstanceIdSet.add(instanceId);
2788
+ }
2789
+ const compositeInstanceQueue = Array.from(this.affectedInstanceIdSet);
2790
+ while (compositeInstanceQueue.length > 0) {
2791
+ const childId = compositeInstanceQueue.pop();
2792
+ const children = this.stateChildIdMap.get(childId) ?? [];
2793
+ for (const child of children) {
2794
+ compositeInstanceQueue.push(child);
2795
+ this.affectedInstanceIdSet.add(child);
2796
+ }
2797
+ }
2798
+ for (const instanceId of this.affectedInstanceIdSet) {
2799
+ let state = this.stateMap.get(instanceId);
2800
+ while (state?.parentId) {
2801
+ this.affectedInstanceIdSet.add(state.parentId);
2802
+ state = this.stateMap.get(state.parentId);
2803
+ }
2804
+ }
2805
+ this.operation.affectedInstanceIds = Array.from(this.affectedInstanceIdSet);
2806
+ }
2807
+ getSourceHashIfApplicable(instance, component) {
2808
+ if (isUnitModel(component)) {
2809
+ return this.unitSourceHashMap.get(instance.type);
2810
+ }
2811
+ return void 0;
2812
+ }
2813
+ async getUpToDateInputHash(instance) {
2814
+ const component = this.library.components[instance.type];
2815
+ this.inputHashResolverInputs.set(instance.id, {
2816
+ instance,
2817
+ component,
2818
+ resolvedInputs: this.resolvedInstanceInputs.get(instance.id),
2819
+ state: this.stateMap.get(instance.id),
2820
+ sourceHash: this.getSourceHashIfApplicable(instance, component)
2821
+ });
2822
+ this.inputHashResolverPromiseCache.delete(instance.id);
2823
+ const { inputHash } = await this.inputHashResolver(instance.id);
2824
+ return inputHash;
2825
+ }
2826
+ getLockInstanceIds() {
2827
+ const instanceIds = new Set(this.operation.affectedInstanceIds);
2828
+ for (const instanceId of this.operation.affectedInstanceIds) {
2829
+ const instance = this.getInstance(instanceId);
2830
+ if (instance.parentId) {
2831
+ instanceIds.add(instance.parentId);
2832
+ }
2833
+ }
2834
+ return Array.from(instanceIds);
2835
+ }
2836
+ static async load(operation, projectBackend, libraryBackend, stateBackend, stateManager, logger, signal) {
2837
+ const [library, unitSources, project, compositeInstances, states] = await Promise.all([
2838
+ libraryBackend.loadLibrary(signal),
2839
+ libraryBackend.getResolvedUnitSources(),
2840
+ projectBackend.getProject(operation.projectId, signal),
2841
+ stateBackend.getCompositeInstances(operation.projectId, signal),
2842
+ stateBackend.getAllInstanceStates(operation.projectId, signal)
2843
+ ]);
2844
+ const workset = new OperationWorkset(
2845
+ operation,
2846
+ library,
2847
+ stateManager,
2848
+ logger.child({ operationId: operation.id, service: "OperationWorkset" })
2849
+ );
2850
+ for (const unitSource of unitSources) {
2851
+ workset.unitSourceHashMap.set(unitSource.unitType, unitSource.sourceHash);
2852
+ }
2853
+ for (const instance of project.instances) {
2854
+ workset.addInstance(instance);
2855
+ }
2856
+ for (const instance of compositeInstances) {
2857
+ const worksetInstance = workset.instanceMap.get(instance.instance.id);
2858
+ if (worksetInstance) {
2859
+ worksetInstance.outputs = instance.instance.outputs;
2860
+ worksetInstance.resolvedOutputs = instance.instance.resolvedOutputs;
2861
+ } else {
2862
+ workset.addInstance(instance.instance);
2863
+ }
2864
+ for (const child of instance.children) {
2865
+ workset.addInstance(child);
2866
+ }
2867
+ }
2868
+ for (const state of states) {
2869
+ if (!workset.instanceMap.has(state.id)) {
2870
+ workset.logger.warn(
2871
+ `ignoring instance "${state.id}" from state because it is not in the project or is not a part of a composite instance`
2872
+ );
2873
+ continue;
2874
+ }
2875
+ workset.setState(state);
2876
+ }
2877
+ for (const instance of workset.instanceMap.values()) {
2878
+ workset.inputResolverInputs.set(`instance:${instance.id}`, {
2879
+ kind: "instance",
2880
+ instance,
2881
+ component: library.components[instance.type]
2882
+ });
2883
+ }
2884
+ for (const hub of project.hubs) {
2885
+ workset.inputResolverInputs.set(`hub:${hub.id}`, { kind: "hub", hub });
2886
+ }
2887
+ workset.inputResolver = createInputResolver(
2888
+ //
2889
+ workset.inputResolverInputs,
2890
+ logger,
2891
+ { promiseCache: workset.inputResolverPromiseCache }
2892
+ );
2893
+ for (const instance of workset.instanceMap.values()) {
2894
+ const output = await workset.inputResolver(`instance:${instance.id}`);
2895
+ if (output.kind !== "instance") {
2896
+ throw new Error("Unexpected output kind");
2897
+ }
2898
+ workset.resolvedInstanceInputs.set(instance.id, output.resolvedInputs);
2899
+ const component = workset.library.components[instance.type];
2900
+ workset.inputHashResolverInputs.set(instance.id, {
2901
+ instance,
2902
+ component,
2903
+ resolvedInputs: output.resolvedInputs,
2904
+ state: workset.stateMap.get(instance.id),
2905
+ sourceHash: workset.getSourceHashIfApplicable(instance, component)
2906
+ });
2907
+ }
2908
+ workset.inputHashResolver = createInputHashResolver(
2909
+ //
2910
+ workset.inputHashResolverInputs,
2911
+ logger,
2912
+ { promiseCache: workset.inputHashResolverPromiseCache }
2913
+ );
2914
+ switch (operation.type) {
2915
+ case "update":
2916
+ case "preview":
2917
+ case "refresh":
2918
+ await workset.extendForUpdate();
2919
+ break;
2920
+ case "recreate":
2921
+ workset.extendForDestroy();
2922
+ break;
2923
+ case "destroy":
2924
+ workset.extendForDestroy();
2925
+ break;
2926
+ }
2927
+ return workset;
2928
+ }
2929
+ }
2930
+
2219
2931
  class RuntimeOperation {
2220
- constructor(operation, runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLock, operationEE, stateEE, compositeInstanceEE, instanceLogsEE, logger) {
2932
+ constructor(operation, runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLock, stateManager, operationEE, instanceLogsEE, logger) {
2221
2933
  this.operation = operation;
2222
2934
  this.runnerBackend = runnerBackend;
2223
2935
  this.stateBackend = stateBackend;
@@ -2225,33 +2937,20 @@ class RuntimeOperation {
2225
2937
  this.projectBackend = projectBackend;
2226
2938
  this.secretBackend = secretBackend;
2227
2939
  this.projectLock = projectLock;
2940
+ this.stateManager = stateManager;
2228
2941
  this.operationEE = operationEE;
2229
- this.stateEE = stateEE;
2230
- this.compositeInstanceEE = compositeInstanceEE;
2231
2942
  this.instanceLogsEE = instanceLogsEE;
2232
2943
  this.logger = logger;
2233
2944
  }
2234
2945
  abortController = new AbortController();
2235
- instanceMap = /* @__PURE__ */ new Map();
2236
- hubMap = /* @__PURE__ */ new Map();
2237
- resolvedInstanceInputs = /* @__PURE__ */ new Map();
2238
- compositeInstanceLock = new BetterLock();
2239
- compositeInstanceMap = /* @__PURE__ */ new Map();
2240
- initialStatusMap = /* @__PURE__ */ new Map();
2241
- stateMap = /* @__PURE__ */ new Map();
2242
2946
  instancePromiseMap = /* @__PURE__ */ new Map();
2243
- childrenStateMap = /* @__PURE__ */ new Map();
2244
- dependentStateMap = /* @__PURE__ */ new Map();
2245
- library;
2246
- inputHashLock = new BetterLock();
2247
- inputHashPromiseCache = /* @__PURE__ */ new Map();
2248
- inputHashNodes = /* @__PURE__ */ new Map();
2249
- resolveInputHash;
2947
+ workset;
2948
+ currentPhase;
2250
2949
  async operateSafe() {
2251
2950
  try {
2252
2951
  await this.operate();
2253
2952
  } catch (error) {
2254
- if (RuntimeOperation.isAbortError(error)) {
2953
+ if (isAbortError(error)) {
2255
2954
  this.logger.info("the operation was cancelled");
2256
2955
  this.operation.status = "cancelled";
2257
2956
  await this.updateOperation();
@@ -2262,121 +2961,64 @@ class RuntimeOperation {
2262
2961
  this.operation.error = errorToString(error);
2263
2962
  await this.updateOperation();
2264
2963
  } finally {
2265
- this.resetPendingStateStatuses();
2266
2964
  await Promise.all([
2267
2965
  this.persistStates.flush(),
2268
2966
  this.persistLogs.flush(),
2269
2967
  this.persistSecrets.flush()
2270
2968
  ]);
2271
2969
  this.logger.debug("operation finished, all entries persisted");
2272
- if (this.operation.type === "preview") {
2273
- const states = await this.stateBackend.getInstanceStates(this.operation.projectId);
2274
- for (const state of states) {
2275
- if (this.operation.instanceIds.includes(state.id)) {
2276
- this.stateEE.emit(this.operation.projectId, createInstanceStateFrontendPatch(state));
2277
- }
2278
- }
2279
- }
2280
- }
2281
- }
2282
- resetPendingStateStatuses() {
2283
- for (const state of this.stateMap.values()) {
2284
- if (state.status !== "pending") {
2285
- continue;
2286
- }
2287
- let initialStatus = this.initialStatusMap.get(state.id);
2288
- let error = state.error;
2289
- if (initialStatus === "destroying" || initialStatus === "pending" || initialStatus === "refreshing" || initialStatus === "updating") {
2290
- initialStatus = "error";
2291
- error ??= "unexpected progressing instance status";
2292
- }
2293
- if (initialStatus) {
2294
- this.updateInstanceState({ id: state.id, status: initialStatus, error });
2295
- }
2296
2970
  }
2297
2971
  }
2298
2972
  async operate() {
2299
2973
  this.logger.info("starting operation");
2300
- const { instances: projectInstances, hubs } = await this.projectBackend.getProject(
2301
- this.operation.projectId
2302
- );
2303
- this.abortController.signal.throwIfAborted();
2304
- const states = await this.stateBackend.getInstanceStates(this.operation.projectId);
2305
- this.abortController.signal.throwIfAborted();
2306
- this.library = await this.libraryBackend.loadLibrary();
2307
- this.logger.info("library ready");
2308
- this.abortController.signal.throwIfAborted();
2309
- const allInstances = projectInstances.filter((instance) => {
2310
- return instance.type in this.library.components;
2311
- });
2312
- const inputResolverNodes = /* @__PURE__ */ new Map();
2313
- for (const hub of hubs) {
2314
- this.hubMap.set(hub.id, hub);
2315
- inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub });
2316
- }
2317
- for (const instance of allInstances) {
2318
- this.instanceMap.set(instance.id, instance);
2319
- inputResolverNodes.set(`instance:${instance.id}`, {
2320
- kind: "instance",
2321
- instance,
2322
- component: this.library.components[instance.type]
2323
- });
2324
- }
2325
- const resolveInputs = createInputResolver(
2326
- inputResolverNodes,
2327
- this.logger.child({ resolver: "input-resolver" })
2328
- );
2329
- for (const instance of allInstances) {
2330
- const output = await resolveInputs(`instance:${instance.id}`);
2331
- if (output.kind !== "instance") {
2332
- throw new Error("Unexpected output kind, expected instance");
2974
+ let lockInstanceIds;
2975
+ while (true) {
2976
+ this.workset = await OperationWorkset.load(
2977
+ this.operation,
2978
+ this.projectBackend,
2979
+ this.libraryBackend,
2980
+ this.stateBackend,
2981
+ this.stateManager,
2982
+ this.logger,
2983
+ this.abortController.signal
2984
+ );
2985
+ lockInstanceIds = this.workset.getLockInstanceIds();
2986
+ if (this.projectLock.canImmediatelyAcquireLocks(lockInstanceIds)) {
2987
+ break;
2333
2988
  }
2334
- this.resolvedInstanceInputs.set(instance.id, output.resolvedInputs);
2335
- }
2336
- for (const state of states) {
2337
- this.stateMap.set(state.id, state);
2338
- this.initialStatusMap.set(state.id, state.status);
2339
- this.tryAddStateToParent(state);
2340
- this.addDependentState(state);
2989
+ this.logger.info("waiting for locks to be available");
2990
+ await this.projectLock.lockInstances(lockInstanceIds, () => Promise.resolve());
2341
2991
  }
2342
- for (const instance of allInstances) {
2343
- this.inputHashNodes.set(instance.id, {
2344
- instance,
2345
- resolvedInputs: this.resolvedInstanceInputs.get(instance.id),
2346
- state: this.stateMap.get(instance.id),
2347
- sourceHash: void 0
2348
- // TODO: implement source hash
2349
- });
2992
+ try {
2993
+ await this.projectLock.lockInstances(lockInstanceIds, () => this.processOperation());
2994
+ } finally {
2995
+ if (this.operation.type === "preview") {
2996
+ this.workset.emitAffectedInitialStates();
2997
+ }
2350
2998
  }
2351
- this.resolveInputHash = createInputHashResolver(
2352
- this.inputHashNodes,
2353
- this.logger.child({ resolver: "input-hash-resolver" }),
2354
- { promiseCache: this.inputHashPromiseCache }
2999
+ }
3000
+ async processOperation() {
3001
+ this.operation.affectedInstanceIds = this.workset.operation.affectedInstanceIds;
3002
+ this.logger.info(
3003
+ { affectedInstanceIds: this.operation.affectedInstanceIds },
3004
+ "operation started"
2355
3005
  );
2356
- if (this.operation.type === "update") {
2357
- await this.extendWithDependencies();
2358
- await this.updateOperation();
2359
- } else if (this.operation.type === "destroy" && this.operation.options.destroyDependentInstances !== false) {
2360
- this.extendWithCreatedDependents();
2361
- await this.updateOperation();
2362
- }
2363
- this.logger.info("all instances loaded", {
2364
- count: allInstances.length,
2365
- affectedCount: this.operation.instanceIds.length
2366
- });
2367
- if (this.operation.type === "evaluate") {
2368
- this.logger.info("evaluating instances");
2369
- await this.evaluateCompositeInstances();
2370
- } else {
3006
+ const phases = this.getOperationPhases();
3007
+ for (const phase of phases) {
3008
+ this.currentPhase = phase;
2371
3009
  const promises = [];
2372
- for (const instanceId of this.operation.instanceIds) {
3010
+ for (const instanceId of this.operation.affectedInstanceIds) {
3011
+ const instance = this.workset.getInstance(instanceId);
3012
+ if (instance.parentId && this.workset.isAffected(instance.parentId)) {
3013
+ continue;
3014
+ }
2373
3015
  promises.push(this.getInstancePromiseForOperation(instanceId));
2374
3016
  }
2375
- this.logger.info("all units operations started");
3017
+ this.logger.info(`all operations for phase "%s" started`, phase);
2376
3018
  this.operation.status = "running";
2377
3019
  await this.updateOperation();
2378
3020
  await Promise.all(promises);
2379
- this.logger.info("all units completed");
3021
+ this.logger.info(`all operations for phase "%s" completed`, phase);
2380
3022
  }
2381
3023
  this.operation.status = "completed";
2382
3024
  this.operation.error = null;
@@ -2388,30 +3030,33 @@ class RuntimeOperation {
2388
3030
  this.abortController.abort();
2389
3031
  }
2390
3032
  getInstancePromiseForOperation(instanceId) {
2391
- const instance = this.instanceMap.get(instanceId);
2392
- if (!instance) {
2393
- throw new Error(`Instance not found: ${instanceId}`);
2394
- }
2395
- const component = this.library.components[instance.type];
2396
- if (!component) {
2397
- throw new Error(`Component not found: ${instance.type}`);
2398
- }
3033
+ const instance = this.workset.getInstance(instanceId);
3034
+ const component = this.workset.library.components[instance.type];
2399
3035
  if (isUnitModel(component)) {
2400
- return this.getUnitPromise(instance, component);
3036
+ return this.getUnitPromise(instance);
2401
3037
  }
2402
3038
  return this.getCompositePromise(instance);
2403
3039
  }
2404
- async getUnitPromise(instance, component) {
3040
+ getOperationPhases() {
2405
3041
  switch (this.operation.type) {
2406
3042
  case "update":
2407
- case "preview": {
2408
- return this.updateUnit(instance, component);
2409
- }
2410
- case "recreate": {
2411
- return this.recreateUnit(instance, component);
3043
+ case "preview":
3044
+ return ["update"];
3045
+ case "recreate":
3046
+ return ["destroy", "update"];
3047
+ case "destroy":
3048
+ return ["destroy"];
3049
+ case "refresh":
3050
+ return ["refresh"];
3051
+ }
3052
+ }
3053
+ async getUnitPromise(instance) {
3054
+ switch (this.currentPhase) {
3055
+ case "update": {
3056
+ return this.updateUnit(instance);
2412
3057
  }
2413
3058
  case "destroy": {
2414
- return this.destroyUnit(instance.id, component);
3059
+ return this.destroyUnit(instance.id);
2415
3060
  }
2416
3061
  case "refresh": {
2417
3062
  return this.refreshUnit(instance.id);
@@ -2421,39 +3066,38 @@ class RuntimeOperation {
2421
3066
  async getCompositePromise(instance) {
2422
3067
  const logger = this.logger.child({ instanceId: instance.id });
2423
3068
  return this.getInstancePromise(instance.id, async () => {
2424
- const initialStatus = this.stateMap.get(instance.id)?.status ?? "not_created";
3069
+ const state = this.workset.getState(instance.id) ?? createInstanceState(instance.id);
2425
3070
  this.updateInstanceState({
2426
- id: instance.id,
3071
+ ...state,
3072
+ parentId: instance.parentId,
2427
3073
  latestOperationId: this.operation.id,
2428
3074
  status: this.getStatusByOperationType(),
2429
- currentResourceCount: 0,
2430
- totalResourceCount: 0
3075
+ error: null
2431
3076
  });
2432
- const { children } = await this.loadCompositeInstance(instance.id);
3077
+ const children = this.workset.getAffectedCompositeChildren(instance.id);
2433
3078
  const childPromises = [];
2434
- if (!children.length) {
2435
- logger.warn("no children found for composite component");
3079
+ if (children.length) {
3080
+ logger.info("running %s children", children.length);
3081
+ } else {
3082
+ logger.warn("no affected children found for composite component");
2436
3083
  }
2437
3084
  for (const child of children) {
2438
- logger.info("waiting for child", { childId: child.id });
3085
+ logger.debug(`waiting for child: "%s"`, child.id);
2439
3086
  childPromises.push(this.getInstancePromiseForOperation(child.id));
2440
3087
  }
2441
3088
  try {
2442
3089
  await Promise.all(childPromises);
2443
- this.abortController.signal.throwIfAborted();
2444
3090
  if (children.length > 0) {
2445
- logger.info("all children completed", { count: children.length });
3091
+ logger.info("all children completed");
2446
3092
  }
2447
3093
  this.updateInstanceState({
2448
3094
  id: instance.id,
2449
- status: "created"
3095
+ status: this.operation.type === "destroy" ? "not_created" : "created",
3096
+ inputHash: await this.workset.getUpToDateInputHash(instance)
2450
3097
  });
2451
3098
  } catch (error) {
2452
- if (RuntimeOperation.isAbortError(error)) {
2453
- this.updateInstanceState({
2454
- id: instance.id,
2455
- status: initialStatus
2456
- });
3099
+ if (isAbortErrorLike(error)) {
3100
+ this.workset.restoreInitialStatus(instance.id);
2457
3101
  return;
2458
3102
  }
2459
3103
  this.updateInstanceState({
@@ -2464,31 +3108,24 @@ class RuntimeOperation {
2464
3108
  }
2465
3109
  });
2466
3110
  }
2467
- updateUnit(instance, component) {
2468
- const logger = this.logger.child({ instanceId: instance.id });
2469
- return this.getInstancePromise(instance.id, async () => {
3111
+ updateUnit(instance) {
3112
+ return this.getInstancePromise(instance.id, async (logger) => {
2470
3113
  this.updateInstanceState({
2471
3114
  id: instance.id,
3115
+ parentId: instance.parentId,
2472
3116
  latestOperationId: this.operation.id,
2473
3117
  status: "pending",
2474
- currentResourceCount: 0,
2475
- totalResourceCount: 0
3118
+ error: null,
3119
+ currentResourceCount: 0
2476
3120
  });
2477
- const dependencies = this.getInstanceDependencies(instance);
2478
- const dependencyPromises = [];
2479
- for (const dependency of dependencies) {
2480
- if (!this.operation.instanceIds.includes(dependency.id)) {
2481
- continue;
2482
- }
2483
- logger.info("waiting for dependency", { dependencyId: dependency.id });
2484
- dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id));
2485
- }
2486
- await Promise.all(dependencyPromises);
2487
- this.abortController.signal.throwIfAborted();
2488
- if (dependencies.length > 0) {
2489
- logger.info("all dependencies completed", { count: dependencies.length });
3121
+ let dependencyIds = [];
3122
+ try {
3123
+ dependencyIds = await this.updateUnitDependencies(instance, logger);
3124
+ } catch (error) {
3125
+ this.workset.restoreInitialStatus(instance.id);
3126
+ throw error;
2490
3127
  }
2491
- logger.info("updating unit...");
3128
+ logger.info("updating unit");
2492
3129
  const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
2493
3130
  this.abortController.signal.throwIfAborted();
2494
3131
  logger.debug("secrets loaded", { count: Object.keys(secrets).length });
@@ -2496,10 +3133,9 @@ class RuntimeOperation {
2496
3133
  projectId: this.operation.projectId,
2497
3134
  instanceType: instance.type,
2498
3135
  instanceName: instance.name,
2499
- config: await this.prepareUnitConfig(instance),
3136
+ config: this.prepareUnitConfig(instance),
2500
3137
  refresh: this.operation.options.refresh,
2501
3138
  secrets: mapValues(secrets, (value) => valueToString(value)),
2502
- source: this.resolveUnitSource(component),
2503
3139
  signal: this.abortController.signal
2504
3140
  });
2505
3141
  logger.debug("unit update requested");
@@ -2510,55 +3146,61 @@ class RuntimeOperation {
2510
3146
  finalStatuses: ["created", "error"]
2511
3147
  });
2512
3148
  await this.watchStateStream(stream);
2513
- const inputHash = await this.getUpToDateInputHash(instance);
3149
+ const inputHash = await this.workset.getUpToDateInputHash(instance);
2514
3150
  this.updateInstanceState({
2515
3151
  id: instance.id,
2516
3152
  inputHash,
2517
- dependencyIds: dependencies.map((dependency) => dependency.id)
3153
+ dependencyIds
2518
3154
  });
2519
3155
  logger.debug("input hash after update", { inputHash });
2520
3156
  logger.info("unit updated");
2521
3157
  });
2522
3158
  }
2523
- async getUpToDateInputHash(instance) {
2524
- return await this.inputHashLock.acquire(async () => {
2525
- this.inputHashNodes.set(instance.id, {
2526
- instance,
2527
- resolvedInputs: this.resolvedInstanceInputs.get(instance.id),
2528
- state: this.stateMap.get(instance.id),
2529
- sourceHash: void 0
2530
- // TODO: implement source hash
2531
- });
2532
- this.inputHashPromiseCache.clear();
2533
- return await this.resolveInputHash(instance.id);
2534
- });
3159
+ async updateUnitDependencies(instance, logger) {
3160
+ try {
3161
+ const dependencies = this.getInstanceDependencies(instance);
3162
+ const dependencyPromises = [];
3163
+ for (const dependency of dependencies) {
3164
+ if (!this.operation.affectedInstanceIds.includes(dependency.id)) {
3165
+ continue;
3166
+ }
3167
+ logger.debug(`waiting for dependency: ${dependency.id}`);
3168
+ dependencyPromises.push(this.getInstancePromiseForOperation(dependency.id));
3169
+ }
3170
+ await Promise.all(dependencyPromises);
3171
+ if (dependencies.length > 0) {
3172
+ logger.info("all dependencies completed");
3173
+ }
3174
+ return dependencies.map((dependency) => dependency.id);
3175
+ } catch (error) {
3176
+ this.workset.restoreInitialStatus(instance.id);
3177
+ throw error;
3178
+ }
2535
3179
  }
2536
- async processBeforeDestroyTriggers(state, component, logger) {
3180
+ async processBeforeDestroyTriggers(state, logger) {
2537
3181
  if (!this.operation.options.invokeDestroyTriggers) {
2538
3182
  logger.debug("destroy triggers are disabled for the operation");
2539
3183
  return;
2540
3184
  }
2541
- const instance = this.instanceMap.get(state.id);
2542
- if (!instance) {
2543
- throw new Error(`Instance not found: ${state.id}`);
2544
- }
3185
+ const instance = this.workset.getInstance(state.id);
2545
3186
  const triggers = state.triggers.filter((trigger) => trigger.spec.type === "before-destroy");
2546
3187
  if (triggers.length === 0) {
2547
3188
  return;
2548
3189
  }
2549
3190
  const invokedTriggers = triggers.map((trigger) => ({ name: trigger.name }));
2550
3191
  logger.info("updating unit to process before-destroy triggers...");
2551
- const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
2552
- this.abortController.signal.throwIfAborted();
2553
- logger.debug({ count: Object.keys(secrets).length }, "secrets loaded");
3192
+ const secrets = await this.secretBackend.get(
3193
+ this.operation.projectId,
3194
+ instance.id,
3195
+ this.abortController.signal
3196
+ );
2554
3197
  await this.runnerBackend.update({
2555
3198
  projectId: this.operation.projectId,
2556
3199
  instanceType: instance.type,
2557
3200
  instanceName: instance.name,
2558
- config: await this.prepareUnitConfig(instance, invokedTriggers),
3201
+ config: this.prepareUnitConfig(instance, invokedTriggers),
2559
3202
  refresh: this.operation.options.refresh,
2560
3203
  secrets: mapValues(secrets, (value) => valueToString(value)),
2561
- source: this.resolveUnitSource(component),
2562
3204
  signal: this.abortController.signal
2563
3205
  });
2564
3206
  logger.debug("unit update requested");
@@ -2571,29 +3213,27 @@ class RuntimeOperation {
2571
3213
  await this.watchStateStream(stream);
2572
3214
  logger.debug("before-destroy triggers processed");
2573
3215
  }
2574
- async destroyUnit(instanceId, component) {
2575
- const logger = this.logger.child({ instanceId });
2576
- return this.getInstancePromise(instanceId, async () => {
3216
+ async destroyUnit(instanceId) {
3217
+ return this.getInstancePromise(instanceId, async (logger) => {
2577
3218
  this.updateInstanceState({
2578
3219
  id: instanceId,
2579
3220
  latestOperationId: this.operation.id,
2580
3221
  status: "pending",
2581
- currentResourceCount: 0,
2582
- totalResourceCount: 0
3222
+ error: null
2583
3223
  });
2584
- const state = this.stateMap.get(instanceId);
3224
+ const state = this.workset.getState(instanceId);
2585
3225
  if (!state) {
2586
3226
  logger.warn("state not found for unit, but destroy was requested");
2587
3227
  return;
2588
3228
  }
2589
3229
  const dependentPromises = [];
2590
- const dependents = this.dependentStateMap.get(instanceId) ?? [];
3230
+ const dependents = this.workset.getDependentStates(state.id);
2591
3231
  for (const dependent of dependents) {
2592
3232
  dependentPromises.push(this.getInstancePromiseForOperation(dependent.id));
2593
3233
  }
2594
3234
  await Promise.all(dependentPromises);
2595
3235
  this.abortController.signal.throwIfAborted();
2596
- await this.processBeforeDestroyTriggers(state, component, logger);
3236
+ await this.processBeforeDestroyTriggers(state, logger);
2597
3237
  logger.info("destroying unit...");
2598
3238
  const [type, name] = parseInstanceId(instanceId);
2599
3239
  await this.runnerBackend.destroy({
@@ -2602,7 +3242,6 @@ class RuntimeOperation {
2602
3242
  instanceName: name,
2603
3243
  refresh: this.operation.options.refresh,
2604
3244
  signal: this.abortController.signal,
2605
- source: this.resolveUnitSource(component),
2606
3245
  deleteUnreachable: this.operation.options.deleteUnreachableResources
2607
3246
  });
2608
3247
  this.logger.debug("destroy request sent");
@@ -2616,74 +3255,6 @@ class RuntimeOperation {
2616
3255
  this.logger.info("unit destroyed");
2617
3256
  });
2618
3257
  }
2619
- async recreateUnit(instance, component) {
2620
- const logger = this.logger.child({ instanceId: instance.id });
2621
- return this.getInstancePromise(instance.id, async () => {
2622
- this.updateInstanceState({
2623
- id: instance.id,
2624
- latestOperationId: this.operation.id,
2625
- status: "pending",
2626
- currentResourceCount: 0,
2627
- totalResourceCount: 0
2628
- });
2629
- const state = this.stateMap.get(instance.id);
2630
- if (!state) {
2631
- logger.warn("state not found for unit, but recreate was requested");
2632
- return;
2633
- }
2634
- await this.processBeforeDestroyTriggers(state, component, logger);
2635
- logger.info("destroying unit...");
2636
- await this.runnerBackend.destroy({
2637
- projectId: this.operation.projectId,
2638
- instanceType: instance.type,
2639
- instanceName: instance.name,
2640
- refresh: this.operation.options.refresh,
2641
- signal: this.abortController.signal,
2642
- source: this.resolveUnitSource(component),
2643
- deleteUnreachable: this.operation.options.deleteUnreachableResources
2644
- });
2645
- logger.debug("destroy request sent");
2646
- const destroyStream = this.runnerBackend.watch({
2647
- projectId: this.operation.projectId,
2648
- instanceType: instance.type,
2649
- instanceName: instance.name,
2650
- finalStatuses: ["not_created", "error"]
2651
- });
2652
- await this.watchStateStream(destroyStream);
2653
- this.abortController.signal.throwIfAborted();
2654
- logger.info("unit destroyed");
2655
- logger.info("updating unit...");
2656
- const secrets = await this.secretBackend.get(this.operation.projectId, instance.id);
2657
- this.abortController.signal.throwIfAborted();
2658
- logger.debug("secrets loaded", { count: Object.keys(secrets).length });
2659
- await this.runnerBackend.update({
2660
- projectId: this.operation.projectId,
2661
- instanceType: instance.type,
2662
- instanceName: instance.name,
2663
- config: await this.prepareUnitConfig(instance),
2664
- refresh: this.operation.options.refresh,
2665
- secrets: mapValues(secrets, (value) => valueToString(value)),
2666
- source: this.resolveUnitSource(component),
2667
- signal: this.abortController.signal
2668
- });
2669
- logger.debug("unit update requested");
2670
- const updateStream = this.runnerBackend.watch({
2671
- projectId: this.operation.projectId,
2672
- instanceType: instance.type,
2673
- instanceName: instance.name,
2674
- finalStatuses: ["created", "error"]
2675
- });
2676
- const inputHash = await this.getUpToDateInputHash(instance);
2677
- const dependencies = this.getInstanceDependencies(instance);
2678
- this.updateInstanceState({
2679
- id: instance.id,
2680
- inputHash,
2681
- dependencyIds: dependencies.map((dependency) => dependency.id)
2682
- });
2683
- await this.watchStateStream(updateStream);
2684
- logger.info("unit recreated");
2685
- });
2686
- }
2687
3258
  async refreshUnit(instanceId) {
2688
3259
  const logger = this.logger.child({ instanceId });
2689
3260
  return this.getInstancePromise(instanceId, async () => {
@@ -2725,84 +3296,23 @@ class RuntimeOperation {
2725
3296
  throw new Error("The stream ended without emitting any state.");
2726
3297
  }
2727
3298
  if (statePatch.status === "error") {
2728
- throw new Error(`The operation on unit "${statePatch.id}" failed: ${statePatch.error}`);
3299
+ throw tryWrapAbortErrorLike(
3300
+ new Error(`The operation on unit "${statePatch.id}" failed: ${statePatch.error}`)
3301
+ );
2729
3302
  }
2730
3303
  }
2731
- async prepareUnitConfig(instance, invokedTriggers = []) {
3304
+ prepareUnitConfig(instance, invokedTriggers = []) {
2732
3305
  const config = {};
2733
3306
  for (const [key, value] of Object.entries(instance.args ?? {})) {
2734
3307
  config[key] = valueToString(value);
2735
3308
  }
2736
- const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
3309
+ const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {};
2737
3310
  for (const [key, value] of Object.entries(instanceInputs)) {
2738
- const resolvedInput = await this.resolveInstanceInput(value.map((input) => input.input));
2739
- config[`input.${key}`] = JSON.stringify(resolvedInput);
3311
+ config[`input.${key}`] = JSON.stringify(value.map((input) => input.input));
2740
3312
  }
2741
3313
  config["$invokedTriggers"] = JSON.stringify(invokedTriggers);
2742
3314
  return config;
2743
3315
  }
2744
- async resolveInstanceInput(input) {
2745
- const promises = input.map((input2) => this.resolveSingleInstanceInput(input2));
2746
- const results = await Promise.all(promises);
2747
- return results.flat();
2748
- }
2749
- async resolveSingleInstanceInput(input) {
2750
- const loadedInstance = this.instanceMap.get(input.instanceId);
2751
- if (loadedInstance && !loadedInstance.parentId) {
2752
- return [input];
2753
- }
2754
- const parentInstance = await this.loadCompositeInstance(input.instanceId);
2755
- const parentOutput = parentInstance.instance.outputs?.[input.output];
2756
- if (!parentOutput) {
2757
- throw new Error(`Output "${input.output}" not found in "${input.instanceId}"`);
2758
- }
2759
- return await this.resolveInstanceInput(parentOutput);
2760
- }
2761
- async loadCompositeInstance(instanceId) {
2762
- return await this.compositeInstanceLock.acquire(instanceId, async () => {
2763
- const existingInstance = this.compositeInstanceMap.get(instanceId);
2764
- if (existingInstance) {
2765
- return existingInstance;
2766
- }
2767
- const compositeInstance = await this.stateBackend.getCompositeInstance(
2768
- this.operation.projectId,
2769
- instanceId
2770
- );
2771
- if (!compositeInstance) {
2772
- throw new Error(`Composite instance not found: ${instanceId}`);
2773
- }
2774
- this.abortController.signal.throwIfAborted();
2775
- for (const child of compositeInstance.children) {
2776
- this.instanceMap.set(child.id, child);
2777
- }
2778
- this.compositeInstanceMap.set(instanceId, compositeInstance);
2779
- return compositeInstance;
2780
- });
2781
- }
2782
- async evaluateCompositeInstances() {
2783
- await this.projectLock.lockInstances(this.operation.instanceIds, async () => {
2784
- const compositeInstances = await this.libraryBackend.evaluateCompositeInstances(
2785
- Array.from(this.instanceMap.values()),
2786
- this.operation.instanceIds
2787
- );
2788
- this.abortController.signal.throwIfAborted();
2789
- for (const instance of compositeInstances) {
2790
- this.compositeInstanceEE.emit(
2791
- `${this.operation.projectId}/${instance.instance.id}`,
2792
- instance
2793
- );
2794
- }
2795
- await this.stateBackend.putCompositeInstances(
2796
- this.operation.projectId,
2797
- await Promise.all(
2798
- compositeInstances.map(async (instance) => ({
2799
- ...instance,
2800
- inputHash: await this.resolveInputHash(instance.instance.id)
2801
- }))
2802
- )
2803
- );
2804
- });
2805
- }
2806
3316
  async updateOperation() {
2807
3317
  this.operationEE.emit(this.operation.projectId, this.operation);
2808
3318
  await this.stateBackend.putOperation(this.operation);
@@ -2812,139 +3322,32 @@ class RuntimeOperation {
2812
3322
  throw new Error("The ID of the instance state is required.");
2813
3323
  }
2814
3324
  if (patch.logLine) {
2815
- this.persistLogs.call([patch.id, patch.logLine]);
2816
- this.instanceLogsEE.emit(`${this.operation.id}/${patch.id}`, patch.logLine);
3325
+ let instance = this.workset.getInstance(patch.id);
3326
+ for (; ; ) {
3327
+ this.persistLogs.call([instance.id, patch.logLine]);
3328
+ this.instanceLogsEE.emit(`${this.operation.id}/${instance.id}`, patch.logLine);
3329
+ if (!instance.parentId) {
3330
+ break;
3331
+ }
3332
+ instance = this.workset.getInstance(instance.parentId);
3333
+ }
2817
3334
  return;
2818
3335
  }
2819
- const state = applyPartialInstanceState(this.stateMap, patch);
3336
+ const state = this.workset.updateState(patch);
2820
3337
  if (this.operation.type !== "preview") {
2821
3338
  this.persistStates.call(state);
2822
3339
  if (patch.secrets) {
2823
3340
  this.persistSecrets.call([patch.id, patch.secrets]);
2824
3341
  }
2825
3342
  }
2826
- this.stateEE.emit(this.operation.projectId, createInstanceStateFrontendPatch(patch));
2827
- }
2828
- tryAddStateToParent(state) {
2829
- if (!state.parentId) {
2830
- return;
2831
- }
2832
- const children = this.childrenStateMap.get(state.parentId) ?? [];
2833
- children.push(state);
2834
- this.childrenStateMap.set(state.parentId, children);
2835
- }
2836
- removeStateFromParent(state) {
2837
- if (!state.parentId) {
2838
- return;
2839
- }
2840
- const children = this.childrenStateMap.get(state.parentId) ?? [];
2841
- const index = children.findIndex((child) => child.id === state.id);
2842
- if (index !== -1) {
2843
- children.splice(index, 1);
2844
- this.childrenStateMap.set(state.parentId, children);
2845
- }
2846
- }
2847
- addDependentState(state) {
2848
- const instance = this.instanceMap.get(state.id);
2849
- if (!instance) {
2850
- return;
2851
- }
2852
- for (const dependency of state.dependencyIds) {
2853
- const dependents = this.dependentStateMap.get(dependency) ?? [];
2854
- dependents.push(state);
2855
- this.dependentStateMap.set(dependency, dependents);
2856
- }
2857
- }
2858
- removeDependentState(state) {
2859
- const instance = this.instanceMap.get(state.id);
2860
- if (!instance) {
2861
- return;
2862
- }
2863
- const instanceInputs = this.resolvedInstanceInputs.get(state.id) ?? {};
2864
- for (const inputs of Object.values(instanceInputs)) {
2865
- for (const input of inputs) {
2866
- const dependents = this.dependentStateMap.get(input.input.instanceId) ?? [];
2867
- const index = dependents.findIndex((dependent) => dependent.id === state.id);
2868
- if (index !== -1) {
2869
- dependents.splice(index, 1);
2870
- this.dependentStateMap.set(input.input.instanceId, dependents);
2871
- }
2872
- }
2873
- }
2874
- }
2875
- async extendWithDependencies() {
2876
- const instanceIdsSet = /* @__PURE__ */ new Set();
2877
- const traverse = async (instanceId) => {
2878
- if (instanceIdsSet.has(instanceId)) {
2879
- return;
2880
- }
2881
- const instance = this.instanceMap.get(instanceId);
2882
- if (!instance) {
2883
- return;
2884
- }
2885
- const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
2886
- for (const inputs of Object.values(instanceInputs)) {
2887
- for (const input of inputs) {
2888
- await traverse(input.input.instanceId);
2889
- }
2890
- }
2891
- const state = this.stateMap.get(instance.id);
2892
- const expectedInputHash = await this.resolveInputHash(instance.id);
2893
- if (this.operation.options.forceUpdateDependencies) {
2894
- instanceIdsSet.add(instanceId);
2895
- return;
2896
- }
2897
- if (state?.status !== "created" || state.inputHash !== expectedInputHash) {
2898
- instanceIdsSet.add(instanceId);
2899
- }
2900
- };
2901
- for (const instanceId of this.operation.instanceIds) {
2902
- await traverse(instanceId);
2903
- instanceIdsSet.add(instanceId);
2904
- }
2905
- this.operation.instanceIds = Array.from(instanceIdsSet);
2906
- }
2907
- extendWithCreatedDependents() {
2908
- const instanceIdsSet = /* @__PURE__ */ new Set();
2909
- const traverse = (instanceKey) => {
2910
- if (instanceIdsSet.has(instanceKey)) {
2911
- return;
2912
- }
2913
- const state = this.stateMap.get(instanceKey);
2914
- if (!state || state.status === "not_created") {
2915
- return;
2916
- }
2917
- const dependents = this.dependentStateMap.get(instanceKey) ?? [];
2918
- for (const dependent of dependents) {
2919
- traverse(dependent.id);
2920
- instanceIdsSet.add(instanceKey);
2921
- }
2922
- };
2923
- for (const instanceId of this.operation.instanceIds) {
2924
- const instance = this.instanceMap.get(instanceId);
2925
- if (!instance) {
2926
- throw new Error(`Instance not found: ${instanceId}`);
2927
- }
2928
- traverse(instance.id);
2929
- instanceIdsSet.add(instanceId);
2930
- }
2931
- this.operation.instanceIds = Array.from(instanceIdsSet);
2932
3343
  }
2933
3344
  getInstancePromise(instanceId, fn) {
2934
3345
  let instancePromise = this.instancePromiseMap.get(instanceId);
2935
3346
  if (instancePromise) {
2936
3347
  return instancePromise;
2937
3348
  }
2938
- instancePromise = (async () => {
2939
- const wasImmediatelyAcquired = this.projectLock.canImmediatelyAcquireLock(instanceId);
2940
- await this.projectLock.lockInstance(instanceId, async () => {
2941
- if (!wasImmediatelyAcquired) {
2942
- await this.refetchState(instanceId);
2943
- }
2944
- return await fn();
2945
- });
2946
- })();
2947
- instancePromise = instancePromise.finally(() => this.instancePromiseMap.delete(instanceId));
3349
+ const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` });
3350
+ instancePromise = fn(logger).finally(() => this.instancePromiseMap.delete(instanceId));
2948
3351
  this.instancePromiseMap.set(instanceId, instancePromise);
2949
3352
  return instancePromise;
2950
3353
  }
@@ -2960,51 +3363,19 @@ class RuntimeOperation {
2960
3363
  return "destroying";
2961
3364
  case "refresh":
2962
3365
  return "refreshing";
2963
- default:
2964
- throw new Error(`Unexpected operation type: ${this.operation.type}`);
2965
3366
  }
2966
3367
  }
2967
3368
  getInstanceDependencies(instance) {
2968
3369
  const dependencies = [];
2969
- const instanceInputs = this.resolvedInstanceInputs.get(instance.id) ?? {};
3370
+ const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {};
2970
3371
  for (const inputs of Object.values(instanceInputs)) {
2971
3372
  for (const input of inputs) {
2972
- const dependency = this.instanceMap.get(input.input.instanceId);
2973
- if (dependency) {
2974
- dependencies.push(dependency);
2975
- }
3373
+ const dependency = this.workset.getInstance(input.input.instanceId);
3374
+ dependencies.push(dependency);
2976
3375
  }
2977
3376
  }
2978
3377
  return dependencies;
2979
3378
  }
2980
- resolveUnitSource(component) {
2981
- if (!component.source || component.source.type === "local") {
2982
- return {
2983
- type: "local",
2984
- // auto-generate path for local units
2985
- path: component.source?.path ?? component.type.split(".").join("/")
2986
- };
2987
- }
2988
- return component.source;
2989
- }
2990
- async refetchState(instanceId) {
2991
- this.logger.info({ instanceId }, "refetching state since the lock was not immediately acquired");
2992
- const state = await this.stateBackend.getInstanceState(this.operation.projectId, instanceId);
2993
- const oldState = this.stateMap.get(instanceId);
2994
- if (oldState) {
2995
- this.removeStateFromParent(oldState);
2996
- this.removeDependentState(oldState);
2997
- }
2998
- if (state) {
2999
- this.stateMap.set(instanceId, state);
3000
- this.initialStatusMap.set(instanceId, state.status);
3001
- this.tryAddStateToParent(state);
3002
- this.addDependentState(state);
3003
- } else {
3004
- this.stateMap.delete(instanceId);
3005
- this.initialStatusMap.delete(instanceId);
3006
- }
3007
- }
3008
3379
  persistStates = createAsyncBatcher(async (states) => {
3009
3380
  this.logger.debug({ msg: "persisting states", count: states.length });
3010
3381
  await this.stateBackend.putAffectedInstanceStates(
@@ -3027,47 +3398,22 @@ class RuntimeOperation {
3027
3398
  }
3028
3399
  }
3029
3400
  );
3030
- static abortMessagePatterns = [
3031
- "Operation aborted",
3032
- "Command was killed with SIGINT"
3033
- ];
3034
- static isAbortError(err) {
3035
- if (isAbortError(err)) {
3036
- return true;
3037
- }
3038
- if (err instanceof Error) {
3039
- return RuntimeOperation.abortMessagePatterns.some((pattern) => err.message.includes(pattern));
3040
- }
3041
- return false;
3042
- }
3043
3401
  }
3044
3402
 
3045
3403
  class OperationManager {
3046
- constructor(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, logger) {
3404
+ constructor(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, stateManager, logger) {
3047
3405
  this.runnerBackend = runnerBackend;
3048
3406
  this.stateBackend = stateBackend;
3049
3407
  this.libraryBackend = libraryBackend;
3050
3408
  this.projectBackend = projectBackend;
3051
3409
  this.secretBackend = secretBackend;
3052
3410
  this.projectLockManager = projectLockManager;
3411
+ this.stateManager = stateManager;
3053
3412
  this.logger = logger;
3054
3413
  }
3055
3414
  operationEE = new EventEmitter();
3056
- stateEE = new EventEmitter();
3057
- compositeInstanceEE = new EventEmitter();
3058
3415
  instanceLogsEE = new EventEmitter();
3059
3416
  runtimeOperations = /* @__PURE__ */ new Map();
3060
- /**
3061
- * Watches for all instance state changes in the project.
3062
- *
3063
- * @param projectId The project ID to watch.
3064
- * @param signal The signal to abort the operation.
3065
- */
3066
- async *watchInstanceStates(projectId, signal) {
3067
- for await (const [state] of on(this.stateEE, projectId, { signal })) {
3068
- yield state;
3069
- }
3070
- }
3071
3417
  /**
3072
3418
  * Watches for all project operations in the project.
3073
3419
  *
@@ -3079,19 +3425,6 @@ class OperationManager {
3079
3425
  yield operation;
3080
3426
  }
3081
3427
  }
3082
- /**
3083
- * Watches for changes in the composite instance.
3084
- *
3085
- * @param projectId The project ID to watch.
3086
- * @param instanceId The instance ID to watch.
3087
- * @param signal The signal to abort the operation.
3088
- */
3089
- async *watchCompositeInstance(projectId, instanceId, signal) {
3090
- const eventKey = `${projectId}/${instanceId}`;
3091
- for await (const [instance] of on(this.compositeInstanceEE, eventKey, { signal })) {
3092
- yield instance;
3093
- }
3094
- }
3095
3428
  /**
3096
3429
  * Watches for logs of the instance in the operation.
3097
3430
  *
@@ -3116,6 +3449,7 @@ class OperationManager {
3116
3449
  projectId: request.projectId,
3117
3450
  type: request.type,
3118
3451
  instanceIds: request.instanceIds,
3452
+ affectedInstanceIds: request.instanceIds,
3119
3453
  status: "pending",
3120
3454
  options: {
3121
3455
  forceUpdateDependencies: request.options?.forceUpdateDependencies ?? false,
@@ -3152,16 +3486,15 @@ class OperationManager {
3152
3486
  this.projectBackend,
3153
3487
  this.secretBackend,
3154
3488
  this.projectLockManager.getLock(operation.projectId),
3489
+ this.stateManager,
3155
3490
  this.operationEE,
3156
- this.stateEE,
3157
- this.compositeInstanceEE,
3158
3491
  this.instanceLogsEE,
3159
3492
  this.logger.child({ service: "RuntimeOperation", operationId: operation.id })
3160
3493
  );
3161
3494
  this.runtimeOperations.set(operation.id, runtimeOperation);
3162
3495
  void runtimeOperation.operateSafe().finally(() => this.runtimeOperations.delete(operation.id));
3163
3496
  }
3164
- static async create(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, logger) {
3497
+ static async create(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend, projectLockManager, stateManager, logger) {
3165
3498
  const operator = new OperationManager(
3166
3499
  runnerBackend,
3167
3500
  stateBackend,
@@ -3169,6 +3502,7 @@ class OperationManager {
3169
3502
  projectBackend,
3170
3503
  secretBackend,
3171
3504
  projectLockManager,
3505
+ stateManager,
3172
3506
  logger.child({ service: "OperationManager" })
3173
3507
  );
3174
3508
  const activeOperations = await stateBackend.getActiveOperations();
@@ -3191,6 +3525,7 @@ async function createServices({
3191
3525
  projectBackend,
3192
3526
  projectManager,
3193
3527
  stateBackend,
3528
+ stateManager,
3194
3529
  operationManager,
3195
3530
  terminalBackend,
3196
3531
  terminalManager,
@@ -3205,10 +3540,11 @@ async function createServices({
3205
3540
  });
3206
3541
  const localPulumiHost = LocalPulumiHost.create(logger);
3207
3542
  const projectLockManager = new ProjectLockManager();
3208
- libraryBackend ??= createLibraryBackend(config, logger);
3543
+ stateManager ??= new StateManager();
3544
+ libraryBackend ??= await createLibraryBackend(config, logger);
3209
3545
  secretBackend ??= await createSecretBackend(config, localPulumiHost, logger);
3210
3546
  stateBackend ??= await createStateBackend(config, localPulumiHost, logger);
3211
- runnerBackend ??= await createRunnerBackend(config, localPulumiHost);
3547
+ runnerBackend ??= createRunnerBackend(config, localPulumiHost, libraryBackend);
3212
3548
  projectBackend ??= await createProjectBackend(config);
3213
3549
  terminalBackend ??= createTerminalBackend(config, logger);
3214
3550
  terminalManager ??= TerminalManager.create(terminalBackend, stateBackend, runnerBackend, logger);
@@ -3219,13 +3555,15 @@ async function createServices({
3219
3555
  projectBackend,
3220
3556
  secretBackend,
3221
3557
  projectLockManager,
3558
+ stateManager,
3222
3559
  logger
3223
3560
  );
3224
3561
  projectManager ??= ProjectManager.create(
3225
3562
  projectBackend,
3226
3563
  stateBackend,
3227
- operationManager,
3228
3564
  libraryBackend,
3565
+ projectLockManager,
3566
+ stateManager,
3229
3567
  logger
3230
3568
  );
3231
3569
  workspaceBackend ??= await createWorkspaceBackend(config);
@@ -3238,6 +3576,7 @@ async function createServices({
3238
3576
  projectBackend,
3239
3577
  projectManager,
3240
3578
  stateBackend,
3579
+ stateManager,
3241
3580
  operationManager,
3242
3581
  terminalBackend,
3243
3582
  terminalManager,