@highstate/backend 0.9.15 → 0.9.16

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 (144) hide show
  1. package/dist/chunk-RCB4AFGD.js +159 -0
  2. package/dist/chunk-RCB4AFGD.js.map +1 -0
  3. package/dist/chunk-WHALQHEZ.js +2017 -0
  4. package/dist/chunk-WHALQHEZ.js.map +1 -0
  5. package/dist/highstate.manifest.json +3 -3
  6. package/dist/index.js +6158 -2178
  7. package/dist/index.js.map +1 -1
  8. package/dist/library/worker/main.js +47 -155
  9. package/dist/library/worker/main.js.map +1 -1
  10. package/dist/shared/index.js +159 -41
  11. package/package.json +25 -7
  12. package/src/artifact/abstractions.ts +46 -0
  13. package/src/artifact/encryption.ts +69 -0
  14. package/src/artifact/factory.ts +36 -0
  15. package/src/artifact/index.ts +3 -0
  16. package/src/artifact/local.ts +142 -0
  17. package/src/business/api-key.ts +65 -0
  18. package/src/business/artifact.ts +288 -0
  19. package/src/business/backend-unlock.ts +10 -0
  20. package/src/business/index.ts +9 -0
  21. package/src/business/instance-lock.ts +124 -0
  22. package/src/business/instance-state.ts +292 -0
  23. package/src/business/operation.ts +251 -0
  24. package/src/business/project-unlock.ts +242 -0
  25. package/src/business/secret.ts +187 -0
  26. package/src/business/worker.ts +161 -0
  27. package/src/common/index.ts +2 -1
  28. package/src/common/performance.ts +44 -0
  29. package/src/common/tree.ts +33 -0
  30. package/src/common/utils.ts +40 -1
  31. package/src/config.ts +14 -10
  32. package/src/hotstate/abstractions.ts +48 -0
  33. package/src/hotstate/factory.ts +17 -0
  34. package/src/{secret → hotstate}/index.ts +1 -0
  35. package/src/hotstate/manager.ts +192 -0
  36. package/src/hotstate/memory.ts +100 -0
  37. package/src/hotstate/validation.ts +101 -0
  38. package/src/index.ts +2 -1
  39. package/src/library/abstractions.ts +10 -23
  40. package/src/library/factory.ts +2 -2
  41. package/src/library/local.ts +89 -102
  42. package/src/library/worker/evaluator.ts +9 -42
  43. package/src/library/worker/loader.lite.ts +41 -0
  44. package/src/library/worker/main.ts +14 -65
  45. package/src/library/worker/protocol.ts +8 -24
  46. package/src/lock/abstractions.ts +6 -0
  47. package/src/lock/factory.ts +15 -0
  48. package/src/{workspace → lock}/index.ts +1 -0
  49. package/src/lock/manager.ts +82 -0
  50. package/src/lock/memory.ts +19 -0
  51. package/src/orchestrator/manager.ts +129 -82
  52. package/src/orchestrator/operation-workset.ts +168 -77
  53. package/src/orchestrator/operation.ts +967 -284
  54. package/src/project/abstractions.ts +20 -7
  55. package/src/project/factory.ts +1 -1
  56. package/src/project/index.ts +0 -1
  57. package/src/project/local.ts +73 -17
  58. package/src/project/manager.ts +272 -131
  59. package/src/pubsub/abstractions.ts +13 -0
  60. package/src/pubsub/factory.ts +19 -0
  61. package/src/pubsub/index.ts +3 -0
  62. package/src/pubsub/local.ts +36 -0
  63. package/src/pubsub/manager.ts +100 -0
  64. package/src/pubsub/validation.ts +33 -0
  65. package/src/runner/abstractions.ts +135 -68
  66. package/src/runner/artifact-env.ts +160 -0
  67. package/src/runner/factory.ts +20 -5
  68. package/src/runner/force-abort.ts +117 -0
  69. package/src/runner/local.ts +281 -371
  70. package/src/{common → runner}/pulumi.ts +86 -37
  71. package/src/services.ts +193 -35
  72. package/src/shared/index.ts +3 -11
  73. package/src/shared/models/backend/index.ts +3 -0
  74. package/src/shared/models/backend/project.ts +63 -0
  75. package/src/shared/models/backend/unlock-method.ts +20 -0
  76. package/src/shared/models/base.ts +151 -0
  77. package/src/shared/models/errors.ts +5 -0
  78. package/src/shared/models/index.ts +4 -0
  79. package/src/shared/models/project/api-key.ts +62 -0
  80. package/src/shared/models/project/artifact.ts +113 -0
  81. package/src/shared/models/project/component.ts +45 -0
  82. package/src/shared/models/project/index.ts +14 -0
  83. package/src/shared/{project.ts → models/project/instance.ts} +12 -0
  84. package/src/shared/models/project/lock.ts +91 -0
  85. package/src/shared/{operation.ts → models/project/operation.ts} +28 -7
  86. package/src/shared/models/project/page.ts +57 -0
  87. package/src/shared/models/project/secret.ts +112 -0
  88. package/src/shared/models/project/service-account.ts +22 -0
  89. package/src/shared/models/project/state.ts +432 -0
  90. package/src/shared/models/project/terminal.ts +99 -0
  91. package/src/shared/models/project/trigger.ts +56 -0
  92. package/src/shared/models/project/unlock-method.ts +31 -0
  93. package/src/shared/models/project/worker.ts +105 -0
  94. package/src/shared/resolvers/graph-resolver.ts +28 -0
  95. package/src/shared/resolvers/index.ts +5 -0
  96. package/src/shared/resolvers/input-hash.ts +53 -15
  97. package/src/shared/resolvers/input.ts +1 -9
  98. package/src/shared/resolvers/registry.ts +3 -2
  99. package/src/shared/resolvers/state.ts +2 -2
  100. package/src/shared/resolvers/validation.ts +61 -20
  101. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  102. package/src/shared/utils/hash.ts +6 -0
  103. package/src/shared/utils/index.ts +3 -0
  104. package/src/shared/utils/promise-tracker.ts +23 -0
  105. package/src/state/abstractions.ts +330 -101
  106. package/src/state/encryption.ts +59 -0
  107. package/src/state/factory.ts +3 -5
  108. package/src/state/index.ts +3 -0
  109. package/src/state/keyring.ts +22 -0
  110. package/src/state/local/backend.ts +299 -0
  111. package/src/state/local/collection.ts +342 -0
  112. package/src/state/local/index.ts +2 -0
  113. package/src/state/manager.ts +804 -18
  114. package/src/state/repository/index.ts +2 -0
  115. package/src/state/repository/repository.index.ts +193 -0
  116. package/src/state/repository/repository.ts +458 -0
  117. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  118. package/src/terminal/docker.ts +18 -14
  119. package/src/terminal/factory.ts +3 -3
  120. package/src/terminal/index.ts +1 -1
  121. package/src/terminal/manager.ts +131 -79
  122. package/src/terminal/run.sh.ts +21 -11
  123. package/src/worker/abstractions.ts +42 -0
  124. package/src/worker/docker.ts +83 -0
  125. package/src/worker/factory.ts +20 -0
  126. package/src/worker/index.ts +3 -0
  127. package/src/worker/manager.ts +139 -0
  128. package/dist/chunk-KTGKNSKM.js +0 -979
  129. package/dist/chunk-KTGKNSKM.js.map +0 -1
  130. package/dist/chunk-WXDYCRTT.js +0 -234
  131. package/dist/chunk-WXDYCRTT.js.map +0 -1
  132. package/src/library/worker/loader.ts +0 -114
  133. package/src/preferences/shared.ts +0 -1
  134. package/src/project/lock.ts +0 -39
  135. package/src/secret/abstractions.ts +0 -59
  136. package/src/secret/factory.ts +0 -22
  137. package/src/secret/local.ts +0 -152
  138. package/src/shared/state.ts +0 -247
  139. package/src/shared/terminal.ts +0 -14
  140. package/src/state/local.ts +0 -612
  141. package/src/workspace/abstractions.ts +0 -41
  142. package/src/workspace/factory.ts +0 -14
  143. package/src/workspace/local.ts +0 -54
  144. /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { TerminalRunOptions, TerminalBackend } from "./shared"
1
+ import type { TerminalRunOptions, TerminalBackend } from "./abstractions"
2
2
  import type { Logger } from "pino"
3
3
  import { Readable } from "node:stream"
4
4
  import { tmpdir } from "node:os"
@@ -9,9 +9,9 @@ import spawn from "nano-spawn"
9
9
  import { runScript } from "./run.sh"
10
10
 
11
11
  export const dockerTerminalBackendConfig = z.object({
12
- HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY: z.string().default("docker"),
13
- HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
14
- HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST: z.string().optional(),
12
+ HIGHSTATE_TERMINAL_BACKEND_DOCKER_BINARY: z.string().default("docker"),
13
+ HIGHSTATE_TERMINAL_BACKEND_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
14
+ HIGHSTATE_TERMINAL_BACKEND_DOCKER_HOST: z.string().optional(),
15
15
  })
16
16
 
17
17
  export class DockerTerminalBackend implements TerminalBackend {
@@ -22,7 +22,7 @@ export class DockerTerminalBackend implements TerminalBackend {
22
22
  private readonly logger: Logger,
23
23
  ) {}
24
24
 
25
- async run({ factory, stdin, stdout, screenSize, signal }: TerminalRunOptions): Promise<void> {
25
+ async run({ spec, stdin, stdout, screenSize, signal }: TerminalRunOptions): Promise<void> {
26
26
  const hsTempDir = resolve(tmpdir(), "highstate")
27
27
  await mkdir(hsTempDir, { recursive: true })
28
28
 
@@ -34,19 +34,19 @@ export class DockerTerminalBackend implements TerminalBackend {
34
34
  "-i",
35
35
  "-v",
36
36
  `${runScriptPath}:/run.sh:ro`,
37
- factory.image,
37
+ spec.image,
38
38
  "/bin/bash",
39
39
  "/run.sh",
40
40
  ]
41
41
 
42
42
  const initData = {
43
- command: factory.command,
44
- cwd: factory.cwd,
43
+ command: spec.command,
44
+ cwd: spec.cwd,
45
45
  env: {
46
- ...factory.env,
46
+ ...spec.env,
47
47
  TERM: "xterm-256color",
48
48
  },
49
- files: factory.files ?? {},
49
+ files: spec.files ?? {},
50
50
  screenSize,
51
51
  }
52
52
 
@@ -71,7 +71,11 @@ export class DockerTerminalBackend implements TerminalBackend {
71
71
  childProcess.stdout!.pipe(stdout)
72
72
  childProcess.stderr!.pipe(stdout)
73
73
 
74
- this.logger.info({ pid: childProcess.pid }, "process started")
74
+ if (!childProcess.pid) {
75
+ throw new Error(`Failed to start Docker container without clear response from child process.`)
76
+ }
77
+
78
+ this.logger.info({ processId: childProcess.pid }, "process started")
75
79
 
76
80
  await process
77
81
  }
@@ -81,9 +85,9 @@ export class DockerTerminalBackend implements TerminalBackend {
81
85
  logger: Logger,
82
86
  ): DockerTerminalBackend {
83
87
  return new DockerTerminalBackend(
84
- config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY,
85
- config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO,
86
- config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST,
88
+ config.HIGHSTATE_TERMINAL_BACKEND_DOCKER_BINARY,
89
+ config.HIGHSTATE_TERMINAL_BACKEND_DOCKER_USE_SUDO,
90
+ config.HIGHSTATE_TERMINAL_BACKEND_DOCKER_HOST,
87
91
  logger.child({ backend: "TerminalBackend", service: "DockerTerminalBackend" }),
88
92
  )
89
93
  }
@@ -1,10 +1,10 @@
1
1
  import type { Logger } from "pino"
2
- import type { TerminalBackend } from "./shared"
2
+ import type { TerminalBackend } from "./abstractions"
3
3
  import { z } from "zod"
4
4
  import { DockerTerminalBackend, dockerTerminalBackendConfig } from "./docker"
5
5
 
6
6
  export const terminalBackendConfig = z.object({
7
- HIGHSTATE_BACKEND_TERMINAL_TYPE: z.enum(["docker"]).default("docker"),
7
+ HIGHSTATE_TERMINAL_BACKEND_TYPE: z.enum(["docker"]).default("docker"),
8
8
  ...dockerTerminalBackendConfig.shape,
9
9
  })
10
10
 
@@ -12,7 +12,7 @@ export function createTerminalBackend(
12
12
  config: z.infer<typeof terminalBackendConfig>,
13
13
  logger: Logger,
14
14
  ): TerminalBackend {
15
- switch (config.HIGHSTATE_BACKEND_TERMINAL_TYPE) {
15
+ switch (config.HIGHSTATE_TERMINAL_BACKEND_TYPE) {
16
16
  case "docker": {
17
17
  return DockerTerminalBackend.create(config, logger)
18
18
  }
@@ -1,3 +1,3 @@
1
- export * from "./shared"
1
+ export * from "./abstractions"
2
2
  export * from "./factory"
3
3
  export * from "./manager"
@@ -1,12 +1,16 @@
1
- import type { ScreenSize, TerminalBackend } from "./shared"
1
+ import type { ScreenSize, TerminalBackend } from "./abstractions"
2
2
  import type { Logger } from "pino"
3
- import type { StateBackend, TerminalHistoryEntry } from "../state"
4
- import type { RunnerBackend } from "../runner"
3
+ import type { PubSubManager } from "../pubsub"
4
+ import type { ProjectUnlockService } from "../business"
5
+ import type { StateManager } from "../state"
5
6
  import { PassThrough, type Stream, type Writable } from "node:stream"
6
- import { EventEmitter, on } from "node:events"
7
- import { parseInstanceId } from "@highstate/contract"
8
- import { uuidv7 } from "uuidv7"
9
- import { type InstanceTerminal, type TerminalSession, createAsyncBatcher } from "../shared"
7
+ import { v7 as uuidv7 } from "uuid"
8
+ import {
9
+ type TerminalSession,
10
+ type TerminalSpec,
11
+ createAsyncBatcher,
12
+ type AsyncBatcher,
13
+ } from "../shared"
10
14
  import { isAbortErrorLike } from "../common"
11
15
 
12
16
  type ManagedTerminal = {
@@ -14,30 +18,31 @@ type ManagedTerminal = {
14
18
  readonly abortController: AbortController
15
19
  readonly stdin: PassThrough
16
20
  readonly stdout: PassThrough
21
+ readonly logBatcher: AsyncBatcher<string>
17
22
 
18
23
  refCount: number
19
24
  started: boolean
20
25
  start(screenSize: ScreenSize): void
21
26
  }
22
27
 
23
- type TerminalEvents = {
24
- [sessionId: string]: [terminal: TerminalSession]
25
- }
26
-
27
28
  const notAttachedTerminalLifetime = 5 * 60 * 1000 // 5 minutes
28
29
 
29
30
  export class TerminalManager {
30
31
  private readonly managedTerminals = new Map<string, ManagedTerminal>()
31
32
  private readonly existingSessions = new Map<string, TerminalSession>()
32
- private readonly terminalEE = new EventEmitter<TerminalEvents>()
33
33
 
34
34
  private constructor(
35
35
  private readonly terminalBackend: TerminalBackend,
36
- private readonly stateBackend: StateBackend,
37
- private readonly runnerBackend: RunnerBackend,
36
+ private readonly stateManager: StateManager,
37
+ private readonly pubsubManager: PubSubManager,
38
+ private readonly projectUnlockService: ProjectUnlockService,
38
39
  private readonly logger: Logger,
39
40
  ) {
40
- void this.markFinishedSessions()
41
+ this.projectUnlockService.registerUnlockTask(
42
+ //
43
+ "mark-finished-terminal-sessions",
44
+ projectId => this.markFinishedSessions(projectId),
45
+ )
41
46
  }
42
47
 
43
48
  public isSessionActive(sessionId: string): boolean {
@@ -46,7 +51,6 @@ export class TerminalManager {
46
51
 
47
52
  public async *watchSession(
48
53
  projectId: string,
49
- instanceId: string,
50
54
  sessionId: string,
51
55
  signal?: AbortSignal,
52
56
  ): AsyncIterable<TerminalSession> {
@@ -54,7 +58,7 @@ export class TerminalManager {
54
58
 
55
59
  // if terminal session is not active, just fetch it, return and stop watching
56
60
  if (!managedTerminal) {
57
- const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId)
61
+ const session = await this.stateManager.getTerminalSessionRepository(projectId).get(sessionId)
58
62
 
59
63
  if (!session) {
60
64
  throw new Error(`Terminal session "${sessionId}" not found`)
@@ -66,67 +70,86 @@ export class TerminalManager {
66
70
 
67
71
  yield managedTerminal.session
68
72
 
69
- for await (const [terminalSession] of on(this.terminalEE, sessionId, {
73
+ for await (const terminalSession of this.pubsubManager.subscribe(
74
+ ["active-terminal-session", projectId, sessionId],
70
75
  signal,
71
- }) as AsyncIterable<[TerminalSession]>) {
76
+ )) {
72
77
  yield terminalSession
73
78
 
74
- if (terminalSession.finishedAt) {
79
+ if (terminalSession.meta.finishedAt) {
75
80
  // terminal session is finished, stop watching
76
81
  return
77
82
  }
78
83
  }
79
84
  }
80
85
 
81
- public async createSession(
82
- projectId: string,
83
- instanceId: string,
84
- terminalName: string,
85
- ): Promise<TerminalSession> {
86
- const [instanceType, instanceName] = parseInstanceId(instanceId)
87
-
88
- const factory = await this.runnerBackend.getTerminalFactory(
89
- { projectId, instanceType, instanceName },
90
- terminalName,
91
- )
86
+ public async createSession(projectId: string, terminalId: string): Promise<TerminalSession> {
87
+ // Get the terminal info from info repository for meta data
88
+ const terminalInfoRepo = this.stateManager.getTerminalRepository(projectId)
89
+ const terminal = await terminalInfoRepo.get(terminalId)
92
90
 
93
- if (!factory) {
94
- throw new Error(
95
- `Terminal factory for instance "${instanceId}" with name "${terminalName}" not found`,
96
- )
91
+ if (!terminal) {
92
+ throw new Error(`Terminal "${terminalId}" not found`)
97
93
  }
98
94
 
99
95
  const terminalSession: TerminalSession = {
100
96
  id: uuidv7(),
101
97
  projectId,
102
- instanceId,
103
- terminalName,
104
- terminalTitle: factory.title,
105
- terminalIcon: factory.icon,
106
- createdAt: new Date(),
98
+ instanceId: terminal.instanceId,
99
+ terminalId,
100
+ meta: {
101
+ title: terminal.meta.title,
102
+ description: terminal.meta.description,
103
+ icon: terminal.meta.icon,
104
+ },
107
105
  }
108
106
 
109
- await this.stateBackend.putTerminalSession(projectId, instanceId, terminalSession)
107
+ // Get the terminal spec for execution
108
+ const terminalSpec = await this.stateManager
109
+ .getTerminalSpecRepository(projectId)
110
+ .get(terminalId)
111
+
112
+ if (!terminalSpec) {
113
+ throw new Error(`No spec found for terminal "${terminalId}"`)
114
+ }
115
+
116
+ const batch = this.stateManager.batch()
117
+ const promises: Promise<void>[] = [
118
+ this.stateManager.getTerminalSessionRepository(projectId).putItem(terminalSession, batch),
119
+ this.stateManager
120
+ .getActiveTerminalSessionIndexRepository(projectId)
121
+ .indexRepository.put(terminalSession.id, terminalSession.id, batch),
122
+ ]
123
+
124
+ if (terminal.instanceId) {
125
+ promises.push(
126
+ this.stateManager
127
+ .getInstanceTerminalSessionIndexRepository(projectId, terminal.instanceId)
128
+ .indexRepository.put(terminalSession.id, terminalSession.id, batch),
129
+ )
130
+ }
131
+
132
+ await Promise.allSettled(promises)
133
+ await batch.write()
110
134
  this.logger.info({ msg: "terminal session created", id: terminalSession.id })
111
135
 
112
- this.createManagedTerminal(factory, terminalSession)
136
+ this.createManagedTerminal(terminalSpec, terminalSession)
113
137
 
114
138
  return terminalSession
115
139
  }
116
140
 
117
141
  public async getOrCreateSession(
118
142
  projectId: string,
119
- instanceId: string,
120
- terminalName: string,
143
+ terminalId: string,
121
144
  newSession = false,
122
145
  ): Promise<TerminalSession> {
123
- const existingSession = this.existingSessions.get(`${projectId}:${instanceId}.${terminalName}`)
146
+ const existingSession = this.existingSessions.get(`${projectId}:${terminalId}`)
124
147
 
125
148
  if (existingSession && !newSession) {
126
149
  return existingSession
127
150
  }
128
151
 
129
- return await this.createSession(projectId, instanceId, terminalName)
152
+ return await this.createSession(projectId, terminalId)
130
153
  }
131
154
 
132
155
  public close(sessionId: string): void {
@@ -189,13 +212,11 @@ export class TerminalManager {
189
212
  }
190
213
 
191
214
  private async updateActiveTerminalSessions(): Promise<void> {
192
- await this.stateBackend.putActiveTerminalSessions(
193
- Array.from(this.managedTerminals.values()).map(t => t.session),
194
- )
215
+ // TODO
195
216
  }
196
217
 
197
218
  private createManagedTerminal(
198
- factory: InstanceTerminal,
219
+ spec: TerminalSpec,
199
220
  terminalSession: TerminalSession,
200
221
  ): ManagedTerminal {
201
222
  const managedTerminal: ManagedTerminal = {
@@ -204,13 +225,21 @@ export class TerminalManager {
204
225
  stdin: new PassThrough(),
205
226
  stdout: new PassThrough(),
206
227
 
228
+ logBatcher: createAsyncBatcher(async (entries: string[]) => {
229
+ this.logger.trace({ msg: "persisting terminal log entries", count: entries.length })
230
+
231
+ await this.stateManager
232
+ .getTerminalSessionLineRepository(terminalSession.projectId, terminalSession.id)
233
+ .putMany(entries.map(entry => [uuidv7(), entry]))
234
+ }),
235
+
207
236
  refCount: 0,
208
237
  started: false,
209
238
 
210
239
  start: (screenSize: ScreenSize) => {
211
240
  void this.terminalBackend
212
241
  .run({
213
- factory,
242
+ spec,
214
243
  stdin: managedTerminal.stdin,
215
244
  stdout: managedTerminal.stdout,
216
245
  screenSize,
@@ -231,19 +260,25 @@ export class TerminalManager {
231
260
  this.logger.info({ msg: "managed terminal closed", id: managedTerminal.session.id })
232
261
  this.managedTerminals.delete(managedTerminal.session.id)
233
262
  this.existingSessions.delete(
234
- `${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`,
263
+ `${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
235
264
  )
236
265
 
237
- managedTerminal.session.finishedAt = new Date()
238
- this.terminalEE.emit(managedTerminal.session.id, managedTerminal.session)
239
-
240
- void this.stateBackend.putTerminalSession(
241
- managedTerminal.session.projectId,
242
- managedTerminal.session.instanceId,
266
+ managedTerminal.session.meta.finishedAt = Date.now()
267
+ void this.pubsubManager.publish(
268
+ [
269
+ "active-terminal-session",
270
+ managedTerminal.session.projectId,
271
+ managedTerminal.session.id,
272
+ ],
243
273
  managedTerminal.session,
244
274
  )
245
275
 
276
+ void this.stateManager
277
+ .getTerminalSessionRepository(managedTerminal.session.projectId)
278
+ .putItem(managedTerminal.session)
279
+
246
280
  void this.updateActiveTerminalSessions()
281
+ void managedTerminal.logBatcher.flush()
247
282
  })
248
283
 
249
284
  setTimeout(
@@ -258,12 +293,12 @@ export class TerminalManager {
258
293
  managedTerminal.stdout.on("data", data => {
259
294
  const entry = String(data)
260
295
 
261
- void this.persistHistory.call([terminalSession.id, entry])
296
+ managedTerminal.logBatcher.call(entry)
262
297
  })
263
298
 
264
299
  this.managedTerminals.set(managedTerminal.session.id, managedTerminal)
265
300
  this.existingSessions.set(
266
- `${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`,
301
+ `${managedTerminal.session.projectId}:${managedTerminal.session.terminalId}`,
267
302
  managedTerminal.session,
268
303
  )
269
304
 
@@ -286,9 +321,7 @@ export class TerminalManager {
286
321
 
287
322
  terminal.abortController.abort()
288
323
  this.managedTerminals.delete(terminal.session.id)
289
- this.existingSessions.delete(
290
- `${terminal.session.projectId}:${terminal.session.instanceId}.${terminal.session.terminalName}`,
291
- )
324
+ this.existingSessions.delete(`${terminal.session.projectId}:${terminal.session.terminalId}`)
292
325
  return
293
326
  }
294
327
 
@@ -297,36 +330,55 @@ export class TerminalManager {
297
330
 
298
331
  static create(
299
332
  terminalBackend: TerminalBackend,
300
- stateBackend: StateBackend,
301
- runnerBackend: RunnerBackend,
333
+ stateBackend: StateManager,
334
+ pubsubManager: PubSubManager,
335
+ projectUnlockService: ProjectUnlockService,
302
336
  logger: Logger,
303
337
  ): TerminalManager {
304
338
  return new TerminalManager(
305
339
  terminalBackend,
306
340
  stateBackend,
307
- runnerBackend,
341
+ pubsubManager,
342
+ projectUnlockService,
308
343
  logger.child({ service: "TerminalManager" }),
309
344
  )
310
345
  }
311
346
 
312
- private persistHistory = createAsyncBatcher(async (entries: TerminalHistoryEntry[]) => {
313
- this.logger.trace({ msg: "persisting history entries", count: entries.length })
347
+ private async markFinishedSessions(projectId: string): Promise<void> {
348
+ const activeSessions = await this.stateManager
349
+ .getActiveTerminalSessionIndexRepository(projectId)
350
+ .getAllItems()
314
351
 
315
- // TODO: buffer entries and save them as lines
352
+ if (activeSessions.length === 0) {
353
+ this.logger.debug({ projectId }, "no lost terminal sessions found")
354
+ return
355
+ }
316
356
 
317
- await this.stateBackend.appendTerminalSessionHistory(entries)
318
- })
357
+ const batch = this.stateManager.batch()
319
358
 
320
- private async markFinishedSessions(): Promise<void> {
321
- const sessions = await this.stateBackend.getActiveTerminalSessions()
359
+ for (const session of activeSessions) {
360
+ if (session.meta.finishedAt) {
361
+ // Already marked as finished
362
+ this.logger.debug({ sessionId: session.id }, "session already marked as finished")
363
+ continue
364
+ }
322
365
 
323
- for (const session of sessions) {
324
- await this.stateBackend.putTerminalSession(session.projectId, session.instanceId, {
325
- ...session,
326
- finishedAt: new Date(),
327
- })
366
+ session.meta.finishedAt = Date.now()
367
+
368
+ // mark session as finished and delete from active index
369
+ await Promise.all([
370
+ this.stateManager.getTerminalSessionRepository(projectId).putItem(session, batch),
371
+ this.stateManager
372
+ .getActiveTerminalSessionIndexRepository(projectId)
373
+ .indexRepository.delete(session.id, batch),
374
+ ])
328
375
  }
329
376
 
330
- await this.updateActiveTerminalSessions()
377
+ await batch.write()
378
+
379
+ this.logger.debug(
380
+ { projectId, count: activeSessions.length },
381
+ "marked terminal sessions as finished",
382
+ )
331
383
  }
332
384
  }
@@ -16,20 +16,30 @@ done
16
16
 
17
17
  # Create files
18
18
  for key in "\${filesKeys[@]}"; do
19
- isBinary=$(jq -r ".files[\\"$key\\"].isBinary // false" <<<"$data")
20
- content=$(jq -r ".files[\\"$key\\"].content" <<<"$data")
21
- mode=$(jq -r ".files[\\"$key\\"].mode // 0" <<<"$data")
19
+ contentType=$(jq -r ".files[\\"$key\\"].content.type" <<<"$data")
22
20
 
23
- mkdir -p "$(dirname "$key")"
24
-
25
- if [ "$isBinary" = "true" ]; then
26
- echo "$content" | base64 -d > "$key"
27
- else
28
- echo "$content" > "$key"
21
+ # Skip artifact type files for now
22
+ if [ "$contentType" = "artifact" ]; then
23
+ continue
29
24
  fi
25
+
26
+ # Handle embedded content
27
+ if [ "$contentType" = "embedded" ]; then
28
+ content=$(jq -r ".files[\\"$key\\"].content.value" <<<"$data")
29
+ isBinary=$(jq -r ".files[\\"$key\\"].meta.isBinary // false" <<<"$data")
30
+ mode=$(jq -r ".files[\\"$key\\"].meta.mode // 0" <<<"$data")
31
+
32
+ mkdir -p "$(dirname "$key")"
33
+
34
+ if [ "$isBinary" = "true" ]; then
35
+ echo "$content" | base64 -d > "$key"
36
+ else
37
+ echo "$content" > "$key"
38
+ fi
30
39
 
31
- if [ "$mode" -ne 0 ]; then
32
- chmod $(printf "%o" "$mode") "$key"
40
+ if [ "$mode" -ne 0 ]; then
41
+ chmod $(printf "%o" "$mode") "$key"
42
+ fi
33
43
  fi
34
44
  done
35
45
 
@@ -0,0 +1,42 @@
1
+ import type { Worker } from "../shared"
2
+
3
+ export type WorkerRunOptions = {
4
+ /**
5
+ * The ID of the project to run the worker for.
6
+ */
7
+ projectId: string
8
+
9
+ /**
10
+ * The worker to run.
11
+ */
12
+ worker: Worker
13
+
14
+ /**
15
+ * The token of the API key to pass to the worker.
16
+ */
17
+ apiKey: string
18
+
19
+ /**
20
+ * The path of the API socket to pass to the worker.
21
+ */
22
+ apiPath: string
23
+
24
+ /**
25
+ * The output stream.
26
+ */
27
+ stdout: NodeJS.WritableStream
28
+
29
+ /**
30
+ * The signal to abort the worker.
31
+ */
32
+ signal?: AbortSignal
33
+ }
34
+
35
+ export interface WorkerBackend {
36
+ /**
37
+ * Runs a worker with the given options.
38
+ *
39
+ * @param options The options.
40
+ */
41
+ run(options: WorkerRunOptions): Promise<void>
42
+ }
@@ -0,0 +1,83 @@
1
+ import type { Logger } from "pino"
2
+ import type { WorkerBackend, WorkerRunOptions } from "./abstractions"
3
+ import { Readable } from "node:stream"
4
+ import { z } from "zod"
5
+ import spawn from "nano-spawn"
6
+
7
+ export const dockerWorkerBackendConfig = z.object({
8
+ HIGHSTATE_WORKER_BACKEND_DOCKER_BINARY: z.string().default("docker"),
9
+ HIGHSTATE_WORKER_BACKEND_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
10
+ HIGHSTATE_WORKER_BACKEND_DOCKER_HOST: z.string().optional(),
11
+ })
12
+
13
+ export class DockerWorkerBackend implements WorkerBackend {
14
+ constructor(
15
+ private readonly binary: string,
16
+ private readonly useSudo: boolean,
17
+ private readonly host: string | undefined,
18
+ private readonly logger: Logger,
19
+ ) {}
20
+
21
+ async run({
22
+ projectId,
23
+ worker,
24
+ apiKey,
25
+ apiPath,
26
+ stdout,
27
+ signal,
28
+ }: WorkerRunOptions): Promise<void> {
29
+ const args = [
30
+ "run",
31
+ "-i",
32
+ "-v",
33
+ `${apiPath}:/var/run/highstate.sock`,
34
+ worker.image,
35
+ "/bin/bash",
36
+ "/run.sh",
37
+ ]
38
+
39
+ const initData = {
40
+ projectId,
41
+ workerId: worker.id,
42
+ apiKey,
43
+ apiUrl: "/var/run/highstate.sock",
44
+ }
45
+
46
+ const initDataStream = Readable.from(JSON.stringify(initData) + "\n")
47
+
48
+ if (this.useSudo) {
49
+ args.unshift(this.binary)
50
+ }
51
+
52
+ const process = spawn(this.useSudo ? "sudo" : this.binary, args, {
53
+ env: {
54
+ DOCKER_HOST: this.host,
55
+ },
56
+ signal,
57
+ })
58
+
59
+ const childProcess = await process.nodeChildProcess
60
+
61
+ initDataStream.pipe(childProcess.stdin!)
62
+
63
+ childProcess.stdout!.pipe(stdout)
64
+ childProcess.stderr!.pipe(stdout)
65
+
66
+ if (!childProcess.pid) {
67
+ throw new Error(`Failed to start Docker container without clear response from child process.`)
68
+ }
69
+
70
+ this.logger.info({ processId: childProcess.pid }, "process started")
71
+
72
+ await process
73
+ }
74
+
75
+ static create(config: z.infer<typeof dockerWorkerBackendConfig>, logger: Logger): WorkerBackend {
76
+ return new DockerWorkerBackend(
77
+ config.HIGHSTATE_WORKER_BACKEND_DOCKER_BINARY,
78
+ config.HIGHSTATE_WORKER_BACKEND_DOCKER_USE_SUDO,
79
+ config.HIGHSTATE_WORKER_BACKEND_DOCKER_HOST,
80
+ logger.child({ backend: "WorkerBackend", service: "DockerWorkerBackend" }),
81
+ )
82
+ }
83
+ }
@@ -0,0 +1,20 @@
1
+ import type { Logger } from "pino"
2
+ import type { WorkerBackend } from "./abstractions"
3
+ import { z } from "zod"
4
+ import { DockerWorkerBackend, dockerWorkerBackendConfig } from "./docker"
5
+
6
+ export const workerBackendConfig = z.object({
7
+ HIGHSTATE_WORKER_BACKEND_TYPE: z.enum(["docker"]).default("docker"),
8
+ ...dockerWorkerBackendConfig.shape,
9
+ })
10
+
11
+ export function createWorkerBackend(
12
+ config: z.infer<typeof workerBackendConfig>,
13
+ logger: Logger,
14
+ ): WorkerBackend {
15
+ switch (config.HIGHSTATE_WORKER_BACKEND_TYPE) {
16
+ case "docker": {
17
+ return DockerWorkerBackend.create(config, logger)
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./abstractions"
2
+ export * from "./factory"
3
+ export * from "./manager"