@highstate/backend 0.7.2 → 0.7.3

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 +207 -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 +14 -18
  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 +93 -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
@@ -0,0 +1,605 @@
1
+ import type { ClassicLevel } from "classic-level"
2
+ import type { AbstractSublevel, AbstractValueIteratorOptions } from "abstract-level"
3
+ import type { Logger } from "pino"
4
+ import { resolve } from "node:path"
5
+ import { z } from "zod"
6
+ import { uuidv7 } from "uuidv7"
7
+ import {
8
+ type ProjectOperation,
9
+ type InstanceState,
10
+ projectOperationSchema,
11
+ instanceStateSchema,
12
+ isFinalOperationStatus,
13
+ type TerminalSession,
14
+ terminalSessionSchema,
15
+ compositeInstanceSchema,
16
+ type CompositeInstance,
17
+ } from "../shared"
18
+ import { LocalPulumiHost } from "../common"
19
+ import { type LogEntry, type StateBackend, type TerminalHistoryEntry } from "./abstractions"
20
+
21
+ export const localStateBackendConfig = z.object({
22
+ HIGHSTATE_BACKEND_STATE_LOCAL_DIR: z.string().optional(),
23
+ })
24
+
25
+ type Sublevel<TValue = unknown> = AbstractSublevel<
26
+ ClassicLevel<string, unknown>,
27
+ string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>,
28
+ string,
29
+ TValue
30
+ >
31
+
32
+ /**
33
+ * A state backend that stores the state in a local LevelDB database.
34
+ *
35
+ * It uses the following structure:
36
+ *
37
+ * - `activeOperations/{operationId}` - all active operations, denormalized from `projects/{projectId}/operations`;
38
+ * - `activeTerminalSessionIds` - the IDs of the active terminal sessions;
39
+ *
40
+ * - `projects/{projectId}/operations/{operationId}` - the operations of the project;
41
+ * - `projects/{projectId}/instanceStates/{instanceId}` - the latest states of the instances of the project;
42
+ * - `projects/{projectId}/compositeInstances/{instanceId}` - all evaluated composite instances of the project;
43
+ * - `projects/{projectId}/compositeInstanceInputHashes/{instanceId}` - the input hashes of the composite instances, can be fetched independently;
44
+ * - `projects/{projectId}/topLevelCompositeChildrenIds/{instanceId}` - the IDs of the composite children of the top-level composite instance;
45
+ * - `projects/{projectId}/instances/{instanceId}/terminalSessions/{sessionId}` - the terminal sessions of the instances;
46
+ *
47
+ * - `operations/{operationId}/instanceStates/{instanceID}` - the states of the instances affected by the operation (at the moment of the operation completion);
48
+ * - `operations/{operationId}/instances/{instanceID}/logs/{logId}` - the logs of the instances affected by the operation, logId is a UUIDv7;
49
+ *
50
+ * - `terminalSessions/{sessionId}/history/{lineId}` - the history lines of the terminal session, lineId is a UUIDv7;
51
+ */
52
+ export class LocalStateBackend implements StateBackend {
53
+ constructor(
54
+ private readonly db: ClassicLevel<string, unknown>,
55
+ private readonly logger: Logger,
56
+ ) {
57
+ this.logger.debug({ msg: "initialized", dbLocation: db.location })
58
+ }
59
+
60
+ async getActiveOperations(): Promise<ProjectOperation[]> {
61
+ const sublevel = this.getActiveOperationsSublevel()
62
+
63
+ return this.getAllSublevelItems(sublevel, projectOperationSchema)
64
+ }
65
+
66
+ async getOperations(projectId: string, beforeOperationId?: string): Promise<ProjectOperation[]> {
67
+ const sublevel = this.getProjectOperationsSublevel(projectId)
68
+ const pageSize = 10
69
+
70
+ return await this.getAllSublevelItems(sublevel, projectOperationSchema, pageSize, {
71
+ lt: beforeOperationId,
72
+ reverse: true,
73
+ })
74
+ }
75
+
76
+ async getAllInstanceStates(projectId: string): Promise<InstanceState[]> {
77
+ const sublevel = this.getProjectInstanceStatesSublevel(projectId)
78
+
79
+ return await this.getAllSublevelItems(sublevel, instanceStateSchema)
80
+ }
81
+
82
+ async getInstanceState(projectId: string, instanceID: string): Promise<InstanceState | null> {
83
+ const sublevel = this.getProjectInstanceStatesSublevel(projectId)
84
+
85
+ return await this.getSublevelItem(sublevel, instanceStateSchema, instanceID)
86
+ }
87
+
88
+ async getInstanceStates(projectId: string, instanceIds: string[]): Promise<InstanceState[]> {
89
+ const sublevel = this.getProjectInstanceStatesSublevel(projectId)
90
+
91
+ return await this.getSublevelItems(sublevel, instanceStateSchema, instanceIds)
92
+ }
93
+
94
+ async getAffectedInstanceStates(operationId: string): Promise<InstanceState[]> {
95
+ const sublevel = this.getOperationInstanceStatesSublevel(operationId)
96
+
97
+ return await this.getAllSublevelItems(sublevel, instanceStateSchema)
98
+ }
99
+
100
+ async getInstanceLogs(operationId: string, instanceId: string): Promise<string[]> {
101
+ const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId)
102
+
103
+ return await Array.fromAsync(sublevel.values())
104
+ }
105
+
106
+ async putOperation(operation: ProjectOperation): Promise<void> {
107
+ this.validateItem(projectOperationSchema, operation)
108
+
109
+ const operationsSublevel = this.getProjectOperationsSublevel(operation.projectId)
110
+ const activeOperationsSublevel = this.getActiveOperationsSublevel()
111
+
112
+ if (!isFinalOperationStatus(operation.status)) {
113
+ // if the operation is not completed or failed, it is active,
114
+ // so we put it to the active operations sublevel as well
115
+ await this.db.batch([
116
+ { type: "put", key: operation.id, value: operation, sublevel: operationsSublevel },
117
+ { type: "put", key: operation.id, value: operation, sublevel: activeOperationsSublevel },
118
+ ])
119
+ } else {
120
+ // otherwise, we put it only to the operations sublevel
121
+ // and ensure it is not in the active operations
122
+ await this.db.batch([
123
+ { type: "put", key: operation.id, value: operation, sublevel: operationsSublevel },
124
+ { type: "del", key: operation.id, sublevel: activeOperationsSublevel },
125
+ ])
126
+ }
127
+ }
128
+
129
+ async putAffectedInstanceStates(
130
+ projectId: string,
131
+ operationId: string,
132
+ states: InstanceState[],
133
+ ): Promise<void> {
134
+ this.validateArray(instanceStateSchema, states)
135
+
136
+ const operationInstanceStatesSublevel = this.getOperationInstanceStatesSublevel(operationId)
137
+ const projectInstanceStatesSublevel = this.getProjectInstanceStatesSublevel(projectId)
138
+
139
+ await this.db.batch(
140
+ // put the states to both the operation and project sublevels
141
+ // denormalization is cool
142
+ // this separation is also necessary because the instance states can be updated without operations
143
+ states.flatMap(state => [
144
+ {
145
+ // always put the state to the operation sublevel for history
146
+ type: "put",
147
+ key: state.id,
148
+ value: state,
149
+ sublevel: operationInstanceStatesSublevel,
150
+ },
151
+ {
152
+ type: state.status === "not_created" ? "del" : "put",
153
+ key: state.id,
154
+ value: state,
155
+ sublevel: projectInstanceStatesSublevel,
156
+ },
157
+ ]),
158
+ )
159
+ }
160
+
161
+ async putInstanceStates(projectId: string, states: InstanceState[]): Promise<void> {
162
+ this.validateArray(instanceStateSchema, states)
163
+
164
+ const sublevel = this.getProjectInstanceStatesSublevel(projectId)
165
+
166
+ await sublevel.batch(
167
+ // as i told before, we update the instance states without operations
168
+ // this method is used when upstream instance state changes are detected
169
+ states.map(state => ({
170
+ type: state.status === "not_created" ? "del" : "put",
171
+ key: state.id,
172
+ value: state,
173
+ })),
174
+ )
175
+ }
176
+
177
+ async appendInstanceLogs(operationId: string, logs: LogEntry[]): Promise<void> {
178
+ const sublevels = new Map<string, Sublevel<string>>()
179
+ for (const [instanceId] of logs) {
180
+ if (sublevels.has(instanceId)) {
181
+ continue
182
+ }
183
+
184
+ const sublevel = this.getOperationInstanceLogsSublevel(operationId, instanceId)
185
+ sublevels.set(instanceId, sublevel)
186
+ }
187
+
188
+ await this.db.batch(
189
+ logs.map(([instanceID, line]) => ({
190
+ type: "put",
191
+ key: uuidv7(),
192
+ value: line,
193
+ sublevel: sublevels.get(instanceID)!,
194
+ })),
195
+ )
196
+ }
197
+
198
+ async getCompositeInstances(
199
+ projectId: string,
200
+ signal?: AbortSignal,
201
+ ): Promise<CompositeInstance[]> {
202
+ const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
203
+
204
+ return await this.getAllSublevelItems(
205
+ //
206
+ sublevel,
207
+ compositeInstanceSchema,
208
+ undefined,
209
+ { signal },
210
+ )
211
+ }
212
+
213
+ async getCompositeInstance(
214
+ projectId: string,
215
+ instanceId: string,
216
+ ): Promise<CompositeInstance | null> {
217
+ const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
218
+
219
+ return this.getSublevelItem(sublevel, compositeInstanceSchema, instanceId)
220
+ }
221
+
222
+ async getCompositeInstanceInputHash(
223
+ projectId: string,
224
+ instanceId: string,
225
+ ): Promise<string | null> {
226
+ const sublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId)
227
+ const inputHash = await sublevel.get(instanceId)
228
+
229
+ return inputHash ?? null
230
+ }
231
+
232
+ async putCompositeInstances(projectId: string, instances: CompositeInstance[]): Promise<void> {
233
+ this.validateArray(compositeInstanceSchema, instances)
234
+
235
+ const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
236
+ const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId)
237
+
238
+ await this.db.batch(
239
+ instances.flatMap(instance => [
240
+ {
241
+ type: "put",
242
+ key: instance.instance.id,
243
+ value: compositeInstanceSchema.parse(instance),
244
+ sublevel: sublevel,
245
+ },
246
+ ]),
247
+ )
248
+ }
249
+
250
+ async clearCompositeInstances(projectId: string, instanceIds: string[]): Promise<void> {
251
+ const sublevel = this.getProjectCompositeInstancesSublevel(projectId)
252
+ const inputHashesSublevel = this.getProjectCompositeInstanceInputHashesSublevel(projectId)
253
+
254
+ await this.db.batch(
255
+ instanceIds.flatMap(instanceId => [
256
+ { type: "del", key: instanceId, sublevel },
257
+ { type: "del", key: instanceId, sublevel: inputHashesSublevel },
258
+ ]),
259
+ )
260
+ }
261
+
262
+ async getTopLevelCompositeChildrenIds(
263
+ projectId: string,
264
+ instanceIds: string[],
265
+ ): Promise<Record<string, string[]>> {
266
+ const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId)
267
+ const items = await sublevel.getMany(instanceIds)
268
+ const schema = z.array(z.string()).optional()
269
+
270
+ const result: Record<string, string[]> = {}
271
+ for (let i = 0; i < items.length; i++) {
272
+ const instanceId = instanceIds[i]
273
+ const childrenIds = schema.parse(items[i])
274
+
275
+ result[instanceId] = childrenIds ?? []
276
+ }
277
+
278
+ return result
279
+ }
280
+
281
+ async putTopLevelCompositeChildrenIds(
282
+ projectId: string,
283
+ childrenIds: Record<string, string[]>,
284
+ ): Promise<void> {
285
+ const sublevel = this.getProjectCompositeChildrenIdsSublevel(projectId)
286
+ const schema = z.array(z.string()).optional()
287
+
288
+ await sublevel.batch(
289
+ Object.entries(childrenIds).map(([instanceId, ids]) => ({
290
+ type: "put",
291
+ key: instanceId,
292
+ value: schema.parse(ids),
293
+ })),
294
+ )
295
+ }
296
+
297
+ async getActiveTerminalSessions(): Promise<TerminalSession[]> {
298
+ const data = await this.db.get("activeTerminalSessionIds", { valueEncoding: "json" })
299
+
300
+ return data ? z.array(terminalSessionSchema).parse(data) : []
301
+ }
302
+
303
+ putActiveTerminalSessions(sessions: TerminalSession[]): Promise<void> {
304
+ this.validateArray(terminalSessionSchema, sessions)
305
+
306
+ return this.db.put("activeTerminalSessionIds", sessions, { valueEncoding: "json" })
307
+ }
308
+
309
+ async getTerminalSession(
310
+ projectId: string,
311
+ instanceId: string,
312
+ sessionId: string,
313
+ ): Promise<TerminalSession | null> {
314
+ const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
315
+
316
+ return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId)
317
+ }
318
+
319
+ async getTerminalSessions(projectId: string, instanceId: string): Promise<TerminalSession[]> {
320
+ const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
321
+
322
+ return await this.getAllSublevelItems(sublevel, terminalSessionSchema)
323
+ }
324
+
325
+ async getLastTerminalSession(
326
+ projectId: string,
327
+ instanceId: string,
328
+ sessionId: string,
329
+ ): Promise<TerminalSession | null> {
330
+ const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
331
+
332
+ return await this.getSublevelItem(sublevel, terminalSessionSchema, sessionId)
333
+ }
334
+
335
+ async putTerminalSession(
336
+ projectId: string,
337
+ instanceId: string,
338
+ session: TerminalSession,
339
+ ): Promise<void> {
340
+ this.validateItem(terminalSessionSchema, session)
341
+
342
+ const sublevel = this.getTerminalSessionsSublevel(projectId, instanceId)
343
+ await sublevel.put(session.id, session)
344
+ }
345
+
346
+ async getTerminalSessionHistory(sessionId: string): Promise<string[]> {
347
+ const sublevel = this.getTerminalSessionHistorySublevel(sessionId)
348
+
349
+ return await Array.fromAsync(sublevel.values())
350
+ }
351
+
352
+ async appendTerminalSessionHistory(lines: TerminalHistoryEntry[]): Promise<void> {
353
+ const sublevels = new Map<string, Sublevel<string>>()
354
+ for (const [sessionId] of lines) {
355
+ if (sublevels.has(sessionId)) {
356
+ continue
357
+ }
358
+
359
+ const sublevel = this.getTerminalSessionHistorySublevel(sessionId)
360
+ sublevels.set(sessionId, sublevel)
361
+ }
362
+
363
+ await this.db.batch(
364
+ lines.map(([sessionId, line]) => ({
365
+ type: "put",
366
+ key: uuidv7(),
367
+ value: line,
368
+ sublevel: sublevels.get(sessionId)!,
369
+ })),
370
+ )
371
+ }
372
+
373
+ private async getAllSublevelItems<TSchema extends z.ZodType>(
374
+ sublevel: Sublevel,
375
+ schema: TSchema,
376
+ limit?: number,
377
+ options?: AbstractValueIteratorOptions<string, unknown>,
378
+ ): Promise<z.infer<TSchema>[]> {
379
+ const result: z.infer<TSchema>[] = []
380
+ const iterator = options ? sublevel.iterator(options) : sublevel.iterator()
381
+ const invalidKeys: string[] = []
382
+
383
+ for await (const [key, value] of iterator) {
384
+ const parseResult = schema.safeParse(value)
385
+
386
+ if (!parseResult.success) {
387
+ this.logger.warn({
388
+ msg: "failed to parse item, it will be deleted",
389
+ error: parseResult.error,
390
+ sublevel: sublevel.prefix,
391
+ key,
392
+ })
393
+
394
+ invalidKeys.push(key)
395
+ continue
396
+ }
397
+
398
+ result.push(parseResult.data as z.infer<TSchema>)
399
+ if (limit && result.length >= limit) {
400
+ break
401
+ }
402
+ }
403
+
404
+ if (invalidKeys.length > 0) {
405
+ this.logger.info({
406
+ msg: "deleting invalid items",
407
+ sublevel: sublevel.prefix,
408
+ keyCount: invalidKeys.length,
409
+ })
410
+
411
+ await sublevel.batch(invalidKeys.map(key => ({ type: "del", key })))
412
+ }
413
+
414
+ return result
415
+ }
416
+
417
+ private async getSublevelItem<TSchema extends z.ZodType>(
418
+ sublevel: Sublevel,
419
+ schema: TSchema,
420
+ key: string,
421
+ ): Promise<z.infer<TSchema> | null> {
422
+ const value = await sublevel.get(key)
423
+
424
+ if (!value) {
425
+ return null
426
+ }
427
+
428
+ const parseResult = schema.safeParse(value)
429
+
430
+ if (!parseResult.success) {
431
+ this.logger.warn({
432
+ msg: "failed to parse item, it will be deleted",
433
+ error: parseResult.error,
434
+ sublevel: sublevel.prefix,
435
+ key,
436
+ })
437
+
438
+ await sublevel.del(key)
439
+ return null
440
+ }
441
+
442
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
443
+ return parseResult.data as z.infer<TSchema>
444
+ }
445
+
446
+ private async getSublevelItems<TSchema extends z.ZodType>(
447
+ sublevel: Sublevel,
448
+ schema: TSchema,
449
+ keys: string[],
450
+ ): Promise<z.infer<TSchema>[]> {
451
+ const result: z.infer<TSchema>[] = []
452
+ const invalidKeys: string[] = []
453
+
454
+ for (const key of keys) {
455
+ const value = await sublevel.get(key)
456
+
457
+ if (!value) {
458
+ continue
459
+ }
460
+
461
+ const parseResult = schema.safeParse(value)
462
+
463
+ if (!parseResult.success) {
464
+ this.logger.warn({
465
+ msg: "failed to parse item, it will be deleted",
466
+ error: parseResult.error,
467
+ sublevel: sublevel.prefix,
468
+ key,
469
+ })
470
+
471
+ invalidKeys.push(key)
472
+ continue
473
+ }
474
+
475
+ result.push(parseResult.data as z.infer<TSchema>)
476
+ }
477
+
478
+ if (invalidKeys.length > 0) {
479
+ this.logger.info({
480
+ msg: "deleting invalid items",
481
+ sublevel: sublevel.prefix,
482
+ keyCount: invalidKeys.length,
483
+ })
484
+
485
+ await sublevel.batch(invalidKeys.map(key => ({ type: "del", key })))
486
+ }
487
+
488
+ return result
489
+ }
490
+
491
+ private validateItem<TSchema extends z.ZodType>(schema: TSchema, item: z.infer<TSchema>): void {
492
+ const parseResult = schema.safeParse(item)
493
+
494
+ if (!parseResult.success) {
495
+ this.logger.error({
496
+ msg: "failed to validate item",
497
+ error: parseResult.error,
498
+ item,
499
+ })
500
+
501
+ throw new Error(`Failed to validate item: ${parseResult.error.errors[0].message}`, {
502
+ cause: parseResult.error,
503
+ })
504
+ }
505
+ }
506
+
507
+ private validateArray<TSchema extends z.ZodType>(
508
+ schema: TSchema,
509
+ items: z.infer<TSchema>[],
510
+ ): void {
511
+ for (const item of items) {
512
+ this.validateItem(schema, item)
513
+ }
514
+ }
515
+
516
+ private getActiveOperationsSublevel(): Sublevel {
517
+ return this.getJsonSublevel("activeOperations")
518
+ }
519
+
520
+ private getProjectOperationsSublevel(projectId: string): Sublevel {
521
+ return this.getJsonSublevel(`projects/${projectId}/operations`)
522
+ }
523
+
524
+ private getProjectCompositeInstancesSublevel(projectId: string): Sublevel {
525
+ return this.getJsonSublevel(`projects/${projectId}/compositeInstances`)
526
+ }
527
+
528
+ private getProjectCompositeInstanceInputHashesSublevel(projectId: string): Sublevel<string> {
529
+ return this.getStringSublevel(`projects/${projectId}/compositeInstanceInputHashes`)
530
+ }
531
+
532
+ private getProjectCompositeChildrenIdsSublevel(projectId: string): Sublevel {
533
+ return this.getJsonSublevel(`projects/${projectId}/topLevelCompositeChildrenIds`)
534
+ }
535
+
536
+ private getProjectInstanceStatesSublevel(projectId: string): Sublevel {
537
+ return this.getJsonSublevel(`projects/${projectId}/instanceStates`)
538
+ }
539
+
540
+ private getOperationInstanceStatesSublevel(operationId: string): Sublevel {
541
+ return this.getJsonSublevel(`operations/${operationId}/instanceStates`)
542
+ }
543
+
544
+ private getOperationInstanceLogsSublevel(
545
+ operationId: string,
546
+ instanceId: string,
547
+ ): Sublevel<string> {
548
+ return this.getStringSublevel(`operations/${operationId}/instances/${instanceId}/logs`)
549
+ }
550
+
551
+ private getTerminalSessionsSublevel(projectId: string, instanceId: string): Sublevel {
552
+ return this.getJsonSublevel(`projects/${projectId}/instances/${instanceId}/terminalSessions`)
553
+ }
554
+
555
+ private getTerminalSessionHistorySublevel(sessionId: string): Sublevel<string> {
556
+ return this.getStringSublevel(`terminalSessions/${sessionId}/history`)
557
+ }
558
+
559
+ private getStringSublevel(path: string): Sublevel<string> {
560
+ return this.db.sublevel<string, string>(path, { valueEncoding: "utf8" })
561
+ }
562
+
563
+ private getJsonSublevel(path: string): Sublevel {
564
+ return this.db.sublevel<string, unknown>(path, { valueEncoding: "json" })
565
+ }
566
+
567
+ static async create(
568
+ config: z.infer<typeof localStateBackendConfig>,
569
+ localPulumiHost: LocalPulumiHost,
570
+ logger: Logger,
571
+ ): Promise<StateBackend> {
572
+ const childLogger = logger.child({ backend: "StateBackend", service: "LocalStateBackend" })
573
+
574
+ let location = config.HIGHSTATE_BACKEND_STATE_LOCAL_DIR
575
+ if (!location) {
576
+ const currentUser = await localPulumiHost.getCurrentUser()
577
+
578
+ if (!currentUser) {
579
+ throw new Error(
580
+ "The pulumi state is not specified, please run `pulumi login` or specify the state location manually before restarting the service",
581
+ )
582
+ }
583
+
584
+ if (!currentUser.url) {
585
+ throw new Error(
586
+ "The pulumi user does not have a URL, please specify the state location manually",
587
+ )
588
+ }
589
+
590
+ const path = currentUser.url.replace("file://", "").replace("~", process.env.HOME!)
591
+ location = resolve(path, ".pulumi", ".highstate")
592
+
593
+ childLogger.debug({
594
+ msg: "auto-detected state location",
595
+ pulumiStateUrl: currentUser.url,
596
+ location,
597
+ })
598
+ }
599
+
600
+ const { ClassicLevel } = await import("classic-level")
601
+ const db = new ClassicLevel<string, unknown>(location)
602
+
603
+ return new LocalStateBackend(db, childLogger)
604
+ }
605
+ }
@@ -0,0 +1,33 @@
1
+ import type { InstanceState } from "../shared"
2
+ import EventEmitter, { on } from "node:events"
3
+
4
+ export type StateEvents = Record<string, [Partial<InstanceState>]>
5
+
6
+ export class StateManager {
7
+ private readonly stateEE = new EventEmitter<StateEvents>()
8
+
9
+ /**
10
+ * Watches for all instance state changes in the project.
11
+ *
12
+ * @param projectId The project ID to watch.
13
+ * @param signal The signal to abort the operation.
14
+ */
15
+ public async *watchInstanceStates(
16
+ projectId: string,
17
+ signal?: AbortSignal,
18
+ ): AsyncIterable<Partial<InstanceState>> {
19
+ for await (const [state] of on(this.stateEE, projectId, { signal })) {
20
+ yield state
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Emits a state patch for the instance in the project.
26
+ *
27
+ * @param projectId The project ID to emit the state patch for.
28
+ * @param patch The state patch to emit.
29
+ */
30
+ public emitStatePatch(projectId: string, patch: Partial<InstanceState>): void {
31
+ this.stateEE.emit(projectId, patch)
32
+ }
33
+ }