@highstate/backend 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/{chunk-V2NILDHS.js → chunk-52MY2TCE.js} +347 -19
  2. package/dist/chunk-52MY2TCE.js.map +1 -0
  3. package/dist/{chunk-I7BWSAN6.js → chunk-UAWBPTDW.js} +3 -3
  4. package/dist/{chunk-I7BWSAN6.js.map → chunk-UAWBPTDW.js.map} +1 -1
  5. package/dist/highstate.manifest.json +4 -4
  6. package/dist/index.js +4159 -785
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +5 -2
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +2 -2
  11. package/package.json +7 -7
  12. package/prisma/backend/_schema/object.prisma +12 -0
  13. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  14. package/prisma/project/artifact.prisma +3 -0
  15. package/prisma/project/entity.prisma +125 -0
  16. package/prisma/project/instance.prisma +6 -0
  17. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  18. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  19. package/prisma/project/operation.prisma +3 -0
  20. package/src/business/artifact.test.ts +22 -2
  21. package/src/business/artifact.ts +7 -1
  22. package/src/business/entity-snapshot.test.ts +684 -0
  23. package/src/business/entity-snapshot.ts +904 -0
  24. package/src/business/evaluation.test.ts +56 -0
  25. package/src/business/evaluation.ts +102 -22
  26. package/src/business/global-search.test.ts +344 -0
  27. package/src/business/global-search.ts +902 -0
  28. package/src/business/index.ts +4 -0
  29. package/src/business/instance-lock.ts +58 -74
  30. package/src/business/instance-state.test.ts +15 -1
  31. package/src/business/instance-state.ts +37 -14
  32. package/src/business/object-ref-index.test.ts +140 -0
  33. package/src/business/object-ref-index.ts +193 -0
  34. package/src/business/operation.test.ts +15 -1
  35. package/src/business/operation.ts +4 -0
  36. package/src/business/project-model.ts +154 -13
  37. package/src/business/project-unlock.ts +25 -2
  38. package/src/business/project.ts +9 -0
  39. package/src/business/secret.test.ts +35 -2
  40. package/src/business/secret.ts +32 -9
  41. package/src/business/settings.ts +761 -0
  42. package/src/business/unit-output.test.ts +477 -0
  43. package/src/business/unit-output.ts +461 -0
  44. package/src/business/worker.ts +55 -4
  45. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  47. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  49. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  50. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  51. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  52. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  54. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  56. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  57. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  58. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  59. package/src/database/_generated/project/browser.ts +23 -0
  60. package/src/database/_generated/project/client.ts +23 -0
  61. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  62. package/src/database/_generated/project/enums.ts +8 -0
  63. package/src/database/_generated/project/internal/class.ts +53 -5
  64. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  65. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  66. package/src/database/_generated/project/models/Artifact.ts +199 -11
  67. package/src/database/_generated/project/models/Entity.ts +1274 -0
  68. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  69. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  70. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  71. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  72. package/src/database/_generated/project/models/Operation.ts +148 -3
  73. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  74. package/src/database/_generated/project/models.ts +4 -0
  75. package/src/database/migration.ts +3 -0
  76. package/src/library/worker/evaluator.ts +7 -1
  77. package/src/orchestrator/manager.ts +7 -0
  78. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  79. package/src/orchestrator/operation-context.ts +154 -16
  80. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  81. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  82. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  83. package/src/orchestrator/operation-plan.md +4 -1
  84. package/src/orchestrator/operation-plan.ts +286 -92
  85. package/src/orchestrator/operation-plan.update.test.md +286 -11
  86. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  87. package/src/orchestrator/operation-workset.ts +72 -22
  88. package/src/orchestrator/operation.cancel.test.ts +4 -0
  89. package/src/orchestrator/operation.composite.test.ts +341 -0
  90. package/src/orchestrator/operation.destroy.test.ts +4 -0
  91. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  92. package/src/orchestrator/operation.preview.test.ts +4 -0
  93. package/src/orchestrator/operation.refresh.test.ts +4 -0
  94. package/src/orchestrator/operation.test-utils.ts +52 -13
  95. package/src/orchestrator/operation.ts +228 -68
  96. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  97. package/src/orchestrator/operation.update.skip.test.ts +110 -0
  98. package/src/orchestrator/operation.update.test.ts +4 -0
  99. package/src/orchestrator/plan-test-builder.ts +1 -0
  100. package/src/orchestrator/unit-input-values.test.ts +450 -0
  101. package/src/orchestrator/unit-input-values.ts +281 -0
  102. package/src/pubsub/manager.ts +3 -0
  103. package/src/runner/abstractions.ts +23 -54
  104. package/src/runner/local.ts +109 -85
  105. package/src/services.ts +52 -1
  106. package/src/shared/models/prisma.ts +1 -0
  107. package/src/shared/models/project/entity.ts +121 -0
  108. package/src/shared/models/project/index.ts +1 -0
  109. package/src/shared/models/project/operation.ts +61 -3
  110. package/src/shared/models/project/state.ts +10 -0
  111. package/src/shared/models/project/worker.ts +7 -0
  112. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  113. package/src/shared/resolvers/effective-output-type.ts +162 -0
  114. package/src/shared/resolvers/index.ts +1 -0
  115. package/src/shared/resolvers/input.ts +59 -9
  116. package/src/shared/utils/index.ts +1 -0
  117. package/src/shared/utils/stable-json.ts +41 -0
  118. package/src/terminal/manager.ts +6 -0
  119. package/src/worker/manager.ts +97 -1
  120. package/dist/chunk-V2NILDHS.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  /** biome-ignore-all lint/complexity/useLiteralKeys: не ори на меня */
2
2
 
3
- import type { ConfigMap, OutputMap, Stack } from "@pulumi/pulumi/automation/index.js"
3
+ import type { ConfigMap, Stack } from "@pulumi/pulumi/automation/index.js"
4
4
  import type { Logger } from "pino"
5
5
  import type { ArtifactBackend, ArtifactService } from "../artifact"
6
6
  import type { LibraryBackend, ResolvedUnitSource } from "../library"
@@ -15,26 +15,17 @@ import type {
15
15
  } from "./abstractions"
16
16
  import type { DualAbortSignal } from "./force-abort"
17
17
  import { EventEmitter, on } from "node:events"
18
- import { mkdtemp, rm } from "node:fs/promises"
19
- import { tmpdir } from "node:os"
18
+ import { mkdir, rm } from "node:fs/promises"
19
+ import { cpus } from "node:os"
20
20
  import { join, resolve } from "node:path"
21
- import { crc32 } from "node:zlib"
22
21
  import {
23
22
  getInstanceId,
24
23
  HighstateConfigKey,
25
24
  type InstanceId,
26
- instanceStatusFieldSchema,
27
- unitArtifactId,
28
25
  unitArtifactSchema,
29
- unitPageSchema,
30
- unitTerminalSchema,
31
- unitTriggerSchema,
32
- unitWorkerSchema,
33
26
  } from "@highstate/contract"
34
- import { encode } from "@msgpack/msgpack"
35
- import { sha256 } from "@noble/hashes/sha2"
36
27
  import { ensureDependencyInstalled } from "nypm"
37
- import { mapValues, omitBy } from "remeda"
28
+ import PQueue from "p-queue"
38
29
  import { z } from "zod"
39
30
  import { runWithRetryOnError } from "../common"
40
31
  import {
@@ -51,20 +42,33 @@ type Events = {
51
42
  export const localRunnerBackendConfig = z.object({
52
43
  HIGHSTATE_RUNNER_BACKEND_LOCAL_PRINT_OUTPUT: z.coerce.boolean().default(true),
53
44
  HIGHSTATE_RUNNER_BACKEND_LOCAL_CACHE_DIR: z.string().optional(),
45
+ HIGHSTATE_RUNNER_BACKEND_LOCAL_CONCURRENCY: z.coerce
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .default(Math.max(1, Math.floor(cpus().length / 4))),
54
50
  })
55
51
 
56
52
  export class LocalRunnerBackend implements RunnerBackend {
57
53
  private readonly events = new EventEmitter<Events>()
54
+ private readonly queue: PQueue
55
+
56
+ private static getUnitTempPath(stateId: string): string {
57
+ return join("/tmp", "highstate", stateId)
58
+ }
58
59
 
59
60
  constructor(
60
61
  private readonly printOutput: boolean,
61
62
  private readonly cacheDir: string,
63
+ concurrency: number,
62
64
  private readonly pulumiProjectHost: LocalPulumiHost,
63
65
  private readonly libraryBackend: LibraryBackend,
64
66
  private readonly arttifactManager: ArtifactService,
65
67
  private readonly artifactBackend: ArtifactBackend,
66
68
  private readonly logger: Logger,
67
- ) {}
69
+ ) {
70
+ this.queue = new PQueue({ concurrency })
71
+ }
68
72
 
69
73
  async *watch(options: UnitOptions): AsyncIterable<UnitStateUpdate> {
70
74
  const stream = on(
@@ -84,21 +88,69 @@ export class LocalRunnerBackend implements RunnerBackend {
84
88
  }
85
89
 
86
90
  update(options: UnitUpdateOptions): Promise<void> {
87
- void this.updateWorker(options, false)
91
+ void this.queue
92
+ .add(async () => {
93
+ options.signal?.throwIfAborted()
94
+
95
+ await this.updateWorker(options, false)
96
+ })
97
+ .catch(error => this.handleQueuedTaskError("update", options, error))
88
98
 
89
99
  return Promise.resolve()
90
100
  }
91
101
 
92
102
  preview(options: UnitUpdateOptions): Promise<void> {
93
- void this.updateWorker(options, true)
103
+ void this.queue
104
+ .add(async () => {
105
+ options.signal?.throwIfAborted()
106
+
107
+ await this.updateWorker(options, true)
108
+ })
109
+ .catch(error => this.handleQueuedTaskError("preview", options, error))
110
+
111
+ return Promise.resolve()
112
+ }
113
+
114
+ destroy(options: UnitDestroyOptions): Promise<void> {
115
+ void this.queue
116
+ .add(async () => {
117
+ options.signal?.throwIfAborted()
118
+
119
+ await this.destroyWorker(options)
120
+ })
121
+ .catch(error => this.handleQueuedTaskError("destroy", options, error))
94
122
 
95
123
  return Promise.resolve()
96
124
  }
97
125
 
126
+ refresh(options: UnitOptions): Promise<void> {
127
+ void this.queue
128
+ .add(async () => {
129
+ options.signal?.throwIfAborted()
130
+
131
+ await this.refreshWorker(options)
132
+ })
133
+ .catch(error => this.handleQueuedTaskError("refresh", options, error))
134
+
135
+ return Promise.resolve()
136
+ }
137
+
138
+ private handleQueuedTaskError(operation: string, options: UnitOptions, error: unknown): void {
139
+ if (error instanceof Error && error.name === "AbortError") {
140
+ return
141
+ }
142
+
143
+ this.logger.warn({
144
+ msg: "failed to execute unit runner task",
145
+ operation,
146
+ unitId: LocalRunnerBackend.getInstanceId(options),
147
+ error,
148
+ })
149
+ }
150
+
98
151
  private async updateWorker(options: UnitUpdateOptions, preview: boolean): Promise<void> {
99
152
  const configMap: ConfigMap = {
100
- [HighstateConfigKey.Config]: { value: JSON.stringify(options.config) },
101
- [HighstateConfigKey.Secrets]: { value: JSON.stringify(options.secrets), secret: true },
153
+ [HighstateConfigKey.Config]: { value: JSON.stringify(options.config), secret: true },
102
154
  }
103
155
 
104
156
  const unitId = LocalRunnerBackend.getInstanceId(options)
@@ -109,9 +161,13 @@ export class LocalRunnerBackend implements RunnerBackend {
109
161
  let artifactEnv: ArtifactEnvironment | null = null
110
162
 
111
163
  try {
112
- // create unit-specific temp directory for better cleanup reliability
113
- unitTempPath = await mkdtemp(join(tmpdir(), `highstate-unit-${options.stateId}-`))
114
- childLogger.debug({ msg: "created unit temp directory", unitTempPath })
164
+ unitTempPath = LocalRunnerBackend.getUnitTempPath(options.stateId)
165
+
166
+ // ensure stable temp directory is empty before starting
167
+ await rm(unitTempPath, { recursive: true, force: true })
168
+ await mkdir(unitTempPath, { recursive: true })
169
+
170
+ childLogger.debug({ msg: "prepared unit temp directory", unitTempPath })
115
171
  options.signal?.throwIfAborted()
116
172
 
117
173
  artifactEnv = await setupArtifactEnvironment(
@@ -128,8 +184,8 @@ export class LocalRunnerBackend implements RunnerBackend {
128
184
 
129
185
  const envVars: Record<string, string> = {
130
186
  HIGHSTATE_CACHE_DIR: this.cacheDir,
131
- HIGHSTATE_TEMP_PATH: unitTempPath,
132
187
  PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : "",
188
+ HIGHSTATE_PULUMI_COMMAND: preview ? "preview" : "update",
133
189
  ...options.envVars,
134
190
  }
135
191
 
@@ -208,7 +264,12 @@ export class LocalRunnerBackend implements RunnerBackend {
208
264
  })
209
265
 
210
266
  const outputs = await stack.outputs()
211
- const completionUpdate = this.createCompletionStateUpdate("update", unitId, outputs)
267
+ const completionUpdate: TypedUnitStateUpdate<"completion"> = {
268
+ unitId,
269
+ type: "completion",
270
+ operationType: "update",
271
+ rawOutputs: outputs,
272
+ }
212
273
 
213
274
  if (!preview && outputs["$artifacts"]) {
214
275
  const artifacts = z
@@ -224,17 +285,9 @@ export class LocalRunnerBackend implements RunnerBackend {
224
285
  childLogger,
225
286
  )
226
287
 
227
- completionUpdate.exportedArtifactIds = mapValues(artifacts, artifacts => {
228
- return artifacts.map(artifact => {
229
- if (artifact[unitArtifactId]) {
230
- return artifact[unitArtifactId]
231
- }
232
-
233
- throw new Error(
234
- `Failed to determine artifact ID for artifact with hash ${artifact.hash}`,
235
- )
236
- })
237
- })
288
+ // zod parse returns cloned objects; write back mutated artifacts
289
+ // so symbol-based unitArtifactId values are visible downstream
290
+ outputs["$artifacts"].value = artifacts
238
291
  } else if (preview && outputs["$artifacts"]) {
239
292
  childLogger.debug({ msg: "skipping artifact persistence for preview" })
240
293
  }
@@ -268,12 +321,6 @@ export class LocalRunnerBackend implements RunnerBackend {
268
321
  }
269
322
  }
270
323
 
271
- async destroy(options: UnitDestroyOptions): Promise<void> {
272
- void this.destroyWorker(options)
273
-
274
- return Promise.resolve()
275
- }
276
-
277
324
  async deleteState(options: UnitOptions): Promise<void> {
278
325
  await this.pulumiProjectHost.runEmpty(
279
326
  {
@@ -298,7 +345,18 @@ export class LocalRunnerBackend implements RunnerBackend {
298
345
  private async destroyWorker(options: UnitDestroyOptions): Promise<void> {
299
346
  const unitId = LocalRunnerBackend.getInstanceId(options)
300
347
 
348
+ const childLogger = this.logger.child({ unitId })
349
+ let unitTempPath: string | null = null
350
+
301
351
  try {
352
+ unitTempPath = LocalRunnerBackend.getUnitTempPath(options.stateId)
353
+
354
+ // ensure stable temp directory is empty before starting
355
+ await rm(unitTempPath, { recursive: true, force: true })
356
+ await mkdir(unitTempPath, { recursive: true })
357
+
358
+ childLogger.debug({ msg: "prepared unit temp directory", unitTempPath })
359
+
302
360
  const resolvedSource = await this.getResolvedUnitSource(options)
303
361
  if (!resolvedSource) {
304
362
  throw new Error(`Resolved unit source not found for ${options.instanceType}`)
@@ -315,6 +373,7 @@ export class LocalRunnerBackend implements RunnerBackend {
315
373
  envVars: {
316
374
  HIGHSTATE_CACHE_DIR: this.cacheDir,
317
375
  PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : "",
376
+ HIGHSTATE_PULUMI_COMMAND: "destroy",
318
377
  ...(options.debug && { TF_LOG: "DEBUG" }),
319
378
  },
320
379
  },
@@ -343,6 +402,7 @@ export class LocalRunnerBackend implements RunnerBackend {
343
402
  color: "always",
344
403
  refresh: options.refresh,
345
404
  remove: true,
405
+ runProgram: options.hasResourceHooks ?? true,
346
406
  signal,
347
407
  debug: options.debug,
348
408
 
@@ -399,15 +459,11 @@ export class LocalRunnerBackend implements RunnerBackend {
399
459
  unitId: unitId,
400
460
  message: await pulumiErrorToString(error),
401
461
  })
462
+ } finally {
463
+ await this.cleanupTempPath(unitTempPath, unitId, "destroy", this.logger)
402
464
  }
403
465
  }
404
466
 
405
- refresh(options: UnitOptions): Promise<void> {
406
- void this.refreshWorker(options)
407
-
408
- return Promise.resolve()
409
- }
410
-
411
467
  private async refreshWorker(options: UnitOptions): Promise<void> {
412
468
  const unitId = LocalRunnerBackend.getInstanceId(options)
413
469
 
@@ -496,51 +552,18 @@ export class LocalRunnerBackend implements RunnerBackend {
496
552
  }
497
553
  }
498
554
 
499
- private createCompletionStateUpdate(
500
- opType: "update" | "destroy" | "refresh",
501
- unitId: InstanceId,
502
- outputs: OutputMap,
503
- ): TypedUnitStateUpdate<"completion"> {
504
- const unitOutputs = omitBy(outputs, (_, key) => key.startsWith("$"))
505
-
506
- return {
507
- unitId,
508
- type: "completion",
509
- operationType: opType,
510
-
511
- outputHash: crc32(sha256(encode(unitOutputs))),
512
-
513
- statusFields: outputs["$statusFields"]
514
- ? z.array(instanceStatusFieldSchema).parse(outputs["$statusFields"].value)
515
- : null,
516
-
517
- terminals: outputs["$terminals"]
518
- ? z.array(unitTerminalSchema).parse(outputs["$terminals"].value)
519
- : null,
520
-
521
- pages: outputs["$pages"] ? z.array(unitPageSchema).parse(outputs["$pages"].value) : null,
522
-
523
- triggers: outputs["$triggers"]
524
- ? z.array(unitTriggerSchema).parse(outputs["$triggers"].value)
525
- : null,
526
-
527
- workers: outputs["$workers"]
528
- ? z.array(unitWorkerSchema).parse(outputs["$workers"].value)
529
- : null,
530
-
531
- secrets: outputs["$secrets"]
532
- ? z.record(z.string(), z.unknown()).parse(outputs["$secrets"].value)
533
- : null,
534
- }
535
- }
536
-
537
555
  private async emitCompletionStateUpdate(
538
556
  opType: OperationType,
539
557
  unitId: InstanceId,
540
558
  stack: Stack,
541
559
  ): Promise<TypedUnitStateUpdate<"completion">> {
542
560
  const output = await stack.outputs()
543
- const update = this.createCompletionStateUpdate(opType, unitId, output)
561
+ const update: TypedUnitStateUpdate<"completion"> = {
562
+ unitId,
563
+ type: "completion",
564
+ operationType: opType,
565
+ rawOutputs: output,
566
+ }
544
567
 
545
568
  return this.emitStateUpdate(update)
546
569
  }
@@ -649,6 +672,7 @@ export class LocalRunnerBackend implements RunnerBackend {
649
672
  return new LocalRunnerBackend(
650
673
  config.HIGHSTATE_RUNNER_BACKEND_LOCAL_PRINT_OUTPUT,
651
674
  cacheDir,
675
+ config.HIGHSTATE_RUNNER_BACKEND_LOCAL_CONCURRENCY,
652
676
  pulumiProjectHost,
653
677
  libraryBackend,
654
678
  artifactManager,
package/src/services.ts CHANGED
@@ -5,8 +5,11 @@ import { type ArtifactBackend, ArtifactService, createArtifactBackend } from "./
5
5
  import {
6
6
  ApiKeyService,
7
7
  BackendUnlockService,
8
+ EntitySnapshotService,
9
+ GlobalSearchService,
8
10
  InstanceLockService,
9
11
  InstanceStateService,
12
+ ObjectRefIndexService,
10
13
  OperationService,
11
14
  ProjectModelService,
12
15
  ProjectService,
@@ -15,6 +18,7 @@ import {
15
18
  SettingsService,
16
19
  TerminalSessionService,
17
20
  UnitExtraService,
21
+ UnitOutputService,
18
22
  WorkerService,
19
23
  } from "./business"
20
24
  import { ProjectEvaluationSubsystem } from "./business/evaluation"
@@ -74,7 +78,9 @@ export type Services = {
74
78
 
75
79
  // business services
76
80
  readonly backendUnlockService: BackendUnlockService
81
+ readonly globalSearchService: GlobalSearchService
77
82
  readonly instanceLockService: InstanceLockService
83
+ readonly objectRefIndexService: ObjectRefIndexService
78
84
  readonly projectUnlockService: ProjectUnlockService
79
85
  readonly operationService: OperationService
80
86
  readonly instanceStateService: InstanceStateService
@@ -86,6 +92,8 @@ export type Services = {
86
92
  readonly artifactService: ArtifactService
87
93
  readonly settingsService: SettingsService
88
94
  readonly unitExtraService: UnitExtraService
95
+ readonly entitySnapshotService: EntitySnapshotService
96
+ readonly unitOutputService: UnitOutputService
89
97
  }
90
98
 
91
99
  export interface CreateServicesOptions {
@@ -134,7 +142,9 @@ export async function createServices({
134
142
 
135
143
  // business services
136
144
  backendUnlockService,
145
+ globalSearchService,
137
146
  instanceLockService,
147
+ objectRefIndexService,
138
148
  projectUnlockService,
139
149
  operationService,
140
150
  secretService,
@@ -146,6 +156,8 @@ export async function createServices({
146
156
  projectModelService,
147
157
  settingsService,
148
158
  unitExtraService,
159
+ entitySnapshotService,
160
+ unitOutputService,
149
161
  } = {},
150
162
  }: CreateServicesOptions = {}): Promise<Services> {
151
163
  runtimeId ??= createId()
@@ -166,6 +178,11 @@ export async function createServices({
166
178
  logger,
167
179
  )
168
180
 
181
+ objectRefIndexService ??= new ObjectRefIndexService(
182
+ database,
183
+ logger.child({ service: "ObjectRefIndexService" }),
184
+ )
185
+
169
186
  pubsubBackend ??= createPubSubBackend(config, logger)
170
187
  pubsubManager ??= new PubSubManager(pubsubBackend, logger)
171
188
 
@@ -175,17 +192,24 @@ export async function createServices({
175
192
  libraryBackend ??= await createLibraryBackend(config, logger)
176
193
 
177
194
  artifactBackend ??= await createArtifactBackend(config, database, logger)
178
- artifactService ??= new ArtifactService(database, artifactBackend, logger)
195
+ artifactService ??= new ArtifactService(database, artifactBackend, objectRefIndexService, logger)
179
196
 
180
197
  backendUnlockService ??= new BackendUnlockService(
181
198
  database,
182
199
  logger.child({ service: "BackendUnlockService" }),
183
200
  )
184
201
 
202
+ globalSearchService ??= new GlobalSearchService(
203
+ database,
204
+ projectUnlockBackend,
205
+ logger.child({ service: "GlobalSearchService" }),
206
+ )
207
+
185
208
  secretService ??= new SecretService(
186
209
  database,
187
210
  pubsubManager,
188
211
  libraryBackend,
212
+ objectRefIndexService,
189
213
  logger.child({ service: "SecretService" }),
190
214
  )
191
215
  sessionService ??= new TerminalSessionService(database)
@@ -199,6 +223,11 @@ export async function createServices({
199
223
  logger,
200
224
  )
201
225
 
226
+ unitOutputService ??= new UnitOutputService(
227
+ libraryBackend,
228
+ logger.child({ service: "UnitOutputService" }),
229
+ )
230
+
202
231
  projectModelBackends ??= await createProjectModelBackends(database, logger)
203
232
 
204
233
  instanceLockService ??= new InstanceLockService(
@@ -211,16 +240,28 @@ export async function createServices({
211
240
  database,
212
241
  pubsubManager,
213
242
  projectUnlockBackend,
243
+ objectRefIndexService,
214
244
  config,
215
245
  logger.child({ service: "StateUnlockService" }),
216
246
  )
217
247
 
248
+ projectUnlockService.registerUnlockTask("sync-object-refs", async projectId => {
249
+ await objectRefIndexService.syncProject(projectId)
250
+ })
251
+
218
252
  operationService ??= new OperationService(
219
253
  database,
220
254
  pubsubManager,
255
+ objectRefIndexService,
221
256
  logger.child({ service: "OperationService" }),
222
257
  )
223
258
 
259
+ entitySnapshotService ??= new EntitySnapshotService(
260
+ database,
261
+ objectRefIndexService,
262
+ logger.child({ service: "EntitySnapshotService" }),
263
+ )
264
+
224
265
  apiKeyService ??= new ApiKeyService(database, logger.child({ service: "ApiKeyService" }))
225
266
 
226
267
  terminalBackend ??= createTerminalBackend(config, logger)
@@ -229,6 +270,7 @@ export async function createServices({
229
270
  database,
230
271
  pubsubManager,
231
272
  projectUnlockService,
273
+ objectRefIndexService,
232
274
  logger,
233
275
  )
234
276
 
@@ -262,6 +304,7 @@ export async function createServices({
262
304
  artifactService,
263
305
  unitExtraService,
264
306
  secretService,
307
+ objectRefIndexService,
265
308
  logger.child({ service: "InstanceService" }),
266
309
  )
267
310
 
@@ -280,6 +323,7 @@ export async function createServices({
280
323
  projectModelService,
281
324
  pubsubManager,
282
325
  projectUnlockService,
326
+ objectRefIndexService,
283
327
  logger,
284
328
  )
285
329
 
@@ -291,6 +335,7 @@ export async function createServices({
291
335
  projectModelBackends,
292
336
  libraryBackend,
293
337
  pubsubManager,
338
+ objectRefIndexService,
294
339
  logger.child({ service: "ProjectService" }),
295
340
  )
296
341
 
@@ -305,6 +350,8 @@ export async function createServices({
305
350
  instanceStateService,
306
351
  projectModelService,
307
352
  unitExtraService,
353
+ entitySnapshotService,
354
+ unitOutputService,
308
355
  database,
309
356
  logger,
310
357
  )
@@ -344,7 +391,9 @@ export async function createServices({
344
391
 
345
392
  // business services
346
393
  backendUnlockService,
394
+ globalSearchService,
347
395
  instanceLockService,
396
+ objectRefIndexService,
348
397
  projectUnlockService,
349
398
  operationService,
350
399
  instanceStateService,
@@ -356,6 +405,8 @@ export async function createServices({
356
405
  projectModelService,
357
406
  settingsService,
358
407
  unitExtraService,
408
+ entitySnapshotService,
409
+ unitOutputService,
359
410
  }
360
411
  }
361
412
 
@@ -22,6 +22,7 @@ declare global {
22
22
  type NullableInstanceId = contract.InstanceId | null
23
23
  type InstanceIds = contract.InstanceId[]
24
24
  type InstanceModel = contract.InstanceModel
25
+ type EntityMeta = Partial<contract.CommonObjectMeta>
25
26
 
26
27
  type InstanceArgs = Record<string, unknown>
27
28
  type InstanceResolvedInputs = Record<string, shared.StableInstanceInput[]>
@@ -0,0 +1,121 @@
1
+ import { commonObjectMetaSchema } from "@highstate/contract"
2
+ import { z } from "zod"
3
+ import { collectionQuerySchema } from "../base"
4
+
5
+ const rawEntityMetaSchema = z
6
+ .object({
7
+ identity: z.string().optional(),
8
+ title: z.string().optional(),
9
+ description: z.string().optional(),
10
+ icon: z.string().optional(),
11
+ iconColor: z.string().optional(),
12
+ })
13
+ .passthrough()
14
+
15
+ export const entityOutputSchema = z.object({
16
+ id: z.string(),
17
+ type: z.string(),
18
+ identity: z.string(),
19
+ meta: commonObjectMetaSchema,
20
+
21
+ /**
22
+ * The ID of the last known entity snapshot.
23
+ */
24
+ snapshotId: z.string().optional(),
25
+
26
+ /**
27
+ * The timestamp of the last known entity snapshot.
28
+ */
29
+ createdAt: z.date().optional(),
30
+ })
31
+
32
+ export type EntityOutput = z.infer<typeof entityOutputSchema>
33
+
34
+ export const entityQuerySchema = collectionQuerySchema.extend({
35
+ type: z.string().optional(),
36
+ })
37
+
38
+ export type EntityQuery = z.infer<typeof entityQuerySchema>
39
+
40
+ export const entitySnapshotOutputSchema = z.object({
41
+ id: z.string(),
42
+ meta: commonObjectMetaSchema,
43
+ content: z.unknown(),
44
+ operationId: z.string(),
45
+ stateId: z.string(),
46
+ referencedInOutputs: z.string().array(),
47
+ exportedInOutputs: z.string().array(),
48
+ createdAt: z.date(),
49
+ })
50
+
51
+ export type EntitySnapshotOutput = z.infer<typeof entitySnapshotOutputSchema>
52
+
53
+ export const entitySnapshotListItemOutputSchema = z.object({
54
+ id: z.string(),
55
+ meta: commonObjectMetaSchema,
56
+ operationId: z.string(),
57
+ stateId: z.string(),
58
+ createdAt: z.date(),
59
+ })
60
+
61
+ export type EntitySnapshotListItemOutput = z.infer<typeof entitySnapshotListItemOutputSchema>
62
+
63
+ export const entitySnapshotDetailsOutputSchema = z.object({
64
+ entity: z.object({
65
+ id: z.string(),
66
+ type: z.string(),
67
+ identity: z.string(),
68
+ }),
69
+ snapshot: entitySnapshotOutputSchema,
70
+ })
71
+
72
+ export type EntitySnapshotDetailsOutput = z.infer<typeof entitySnapshotDetailsOutputSchema>
73
+
74
+ export const entityDetailsOutputSchema = z.object({
75
+ ...entityOutputSchema.shape,
76
+
77
+ /**
78
+ * The last known entity snapshot.
79
+ */
80
+ lastSnapshot: entitySnapshotOutputSchema.nullable(),
81
+ })
82
+
83
+ export type EntityDetailsOutput = z.infer<typeof entityDetailsOutputSchema>
84
+
85
+ export const entityReferenceOutputSchema = z.object({
86
+ id: z.string(),
87
+ meta: commonObjectMetaSchema,
88
+ group: z.string(),
89
+
90
+ fromSnapshotId: z.string(),
91
+ fromEntityId: z.string(),
92
+ fromEntityType: z.string(),
93
+ fromEntityIdentity: z.string(),
94
+ fromEntityMeta: commonObjectMetaSchema,
95
+
96
+ toSnapshotId: z.string(),
97
+ toEntityId: z.string(),
98
+ toEntityType: z.string(),
99
+ toEntityIdentity: z.string(),
100
+ toEntityMeta: commonObjectMetaSchema,
101
+ })
102
+
103
+ export type EntityReferenceOutput = z.infer<typeof entityReferenceOutputSchema>
104
+
105
+ export function toCommonEntityMeta(
106
+ meta: unknown | null | undefined,
107
+ ): z.infer<typeof commonObjectMetaSchema> {
108
+ const parsedRawMeta = rawEntityMetaSchema.safeParse(meta)
109
+ if (parsedRawMeta.success) {
110
+ return {
111
+ title: parsedRawMeta.data.title ?? parsedRawMeta.data.identity ?? "Unknown",
112
+ description: parsedRawMeta.data.description,
113
+ icon: parsedRawMeta.data.icon,
114
+ iconColor: parsedRawMeta.data.iconColor,
115
+ }
116
+ }
117
+
118
+ return {
119
+ title: "Unknown",
120
+ }
121
+ }
@@ -1,6 +1,7 @@
1
1
  export * from "./api-key"
2
2
  export * from "./artifact"
3
3
  export * from "./custom-status"
4
+ export * from "./entity"
4
5
  export * from "./lock"
5
6
  export * from "./model"
6
7
  export * from "./operation"