@highstate/backend 0.9.18 → 0.9.20

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 (331) hide show
  1. package/dist/{chunk-OU5OQBLB.js → chunk-I7BWSAN6.js} +3 -28
  2. package/dist/{chunk-OU5OQBLB.js.map → chunk-I7BWSAN6.js.map} +1 -1
  3. package/dist/chunk-RC6Q3XQQ.js +1547 -0
  4. package/dist/chunk-RC6Q3XQQ.js.map +1 -0
  5. package/dist/chunk-VB4YL327.js +139 -0
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +2 -1
  10. package/dist/index.js +7590 -7289
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +1 -1
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +35 -29
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +2 -2
  17. package/package.json +18 -9
  18. package/prisma/backend/_schema/layout.prisma +7 -0
  19. package/prisma/backend/_schema/library.prisma +17 -0
  20. package/prisma/backend/_schema/project.prisma +101 -0
  21. package/prisma/backend/_schema/pulumi.prisma +17 -0
  22. package/prisma/backend/postgresql/main.prisma +17 -0
  23. package/prisma/backend/sqlite/main.prisma +17 -0
  24. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  25. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  26. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  27. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  28. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  29. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  30. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  31. package/prisma/project/api-key.prisma +32 -0
  32. package/prisma/project/artifact.prisma +52 -0
  33. package/prisma/project/custom-status.prisma +46 -0
  34. package/prisma/project/evaluation.prisma +45 -0
  35. package/prisma/project/instance.prisma +157 -0
  36. package/prisma/project/layout.prisma +23 -0
  37. package/prisma/project/lock.prisma +18 -0
  38. package/prisma/project/main.prisma +17 -0
  39. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  40. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  41. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  42. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  43. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  45. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  46. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  47. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  48. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  49. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  50. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  51. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  52. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  53. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  54. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  55. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  56. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  59. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  60. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  61. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  62. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  64. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  65. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  66. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  67. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  68. package/prisma/project/migrations/migration_lock.toml +3 -0
  69. package/prisma/project/model.prisma +37 -0
  70. package/prisma/project/operation.prisma +148 -0
  71. package/prisma/project/page.prisma +49 -0
  72. package/prisma/project/secret.prisma +54 -0
  73. package/prisma/project/service-account.prisma +42 -0
  74. package/prisma/project/terminal.prisma +107 -0
  75. package/prisma/project/trigger.prisma +37 -0
  76. package/prisma/project/unlock-method.prisma +46 -0
  77. package/prisma/project/worker.prisma +169 -0
  78. package/src/artifact/abstractions.ts +13 -13
  79. package/src/artifact/encryption.ts +30 -54
  80. package/src/artifact/factory.ts +6 -9
  81. package/src/artifact/local.ts +33 -46
  82. package/src/business/api-key.ts +24 -36
  83. package/src/business/artifact.test.ts +978 -0
  84. package/src/business/artifact.ts +136 -216
  85. package/src/business/evaluation.ts +328 -0
  86. package/src/business/index.ts +5 -2
  87. package/src/business/instance-lock.test.ts +1060 -0
  88. package/src/business/instance-lock.ts +387 -78
  89. package/src/business/instance-state.test.ts +735 -0
  90. package/src/business/instance-state.ts +582 -337
  91. package/src/business/operation.test.ts +439 -0
  92. package/src/business/operation.ts +174 -208
  93. package/src/business/project-model.ts +258 -0
  94. package/src/business/project-unlock.ts +168 -126
  95. package/src/business/project.ts +287 -179
  96. package/src/business/secret.test.ts +469 -130
  97. package/src/business/secret.ts +177 -217
  98. package/src/business/settings.test.ts +695 -0
  99. package/src/business/settings.ts +855 -0
  100. package/src/business/terminal-session.ts +90 -0
  101. package/src/business/unit-extra.test.ts +539 -0
  102. package/src/business/unit-extra.ts +160 -0
  103. package/src/business/worker.test.ts +356 -579
  104. package/src/business/worker.ts +238 -339
  105. package/src/common/codebase.ts +65 -0
  106. package/src/common/index.ts +3 -5
  107. package/src/common/logger.ts +5 -0
  108. package/src/common/utils.ts +4 -3
  109. package/src/config.ts +10 -11
  110. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  111. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  112. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  113. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  114. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  115. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  116. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  117. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  119. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  120. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  121. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  122. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  123. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  124. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  125. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  126. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  127. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  128. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  129. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  130. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  132. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  133. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  134. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  135. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  136. package/src/database/_generated/project/client.ts +204 -0
  137. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  138. package/src/database/_generated/project/enums.ts +104 -0
  139. package/src/database/_generated/project/internal/class.ts +479 -0
  140. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  141. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  142. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  143. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  144. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  145. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  146. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  147. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  148. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  149. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  150. package/src/database/_generated/project/models/Operation.ts +1647 -0
  151. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  152. package/src/database/_generated/project/models/Page.ts +1838 -0
  153. package/src/database/_generated/project/models/Secret.ts +1692 -0
  154. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  155. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  156. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  157. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  158. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  159. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  160. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  161. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  162. package/src/database/_generated/project/models/Worker.ts +1459 -0
  163. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  164. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  165. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  166. package/src/database/_generated/project/models.ts +35 -0
  167. package/src/database/_generated/project/pjtg.ts +182 -0
  168. package/src/database/abstractions.ts +19 -0
  169. package/src/database/factory.ts +37 -0
  170. package/src/database/index.ts +6 -0
  171. package/src/database/local/backend.ts +134 -0
  172. package/src/database/local/index.ts +3 -0
  173. package/src/database/local/meta.ts +46 -0
  174. package/src/database/local/prisma.config.ts +25 -0
  175. package/src/database/local/project.ts +39 -0
  176. package/src/database/manager.ts +181 -0
  177. package/src/database/migrate.ts +35 -0
  178. package/src/database/prisma.ts +56 -0
  179. package/src/database/well-known.ts +38 -0
  180. package/src/index.ts +4 -4
  181. package/src/library/abstractions.ts +3 -5
  182. package/src/library/factory.ts +1 -1
  183. package/src/library/local.ts +81 -26
  184. package/src/library/package-resolution-worker.ts +1 -1
  185. package/src/library/worker/evaluator.ts +40 -23
  186. package/src/library/worker/loader.lite.ts +1 -1
  187. package/src/library/worker/main.ts +3 -10
  188. package/src/library/worker/protocol.ts +0 -1
  189. package/src/lock/index.ts +0 -1
  190. package/src/lock/manager.ts +0 -10
  191. package/src/orchestrator/manager.ts +190 -104
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +233 -578
  203. package/src/orchestrator/operation.ts +440 -948
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +29 -13
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +40 -41
  219. package/src/runner/artifact-env.ts +19 -8
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +74 -67
  223. package/src/runner/pulumi.ts +23 -63
  224. package/src/services.ts +181 -123
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +9 -1
  227. package/src/shared/models/backend/project.ts +43 -42
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -26
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -59
  236. package/src/shared/models/project/artifact.ts +16 -76
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +8 -7
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +19 -1
  241. package/src/shared/models/project/operation.ts +235 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -89
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -407
  246. package/src/shared/models/project/terminal.ts +75 -88
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +20 -26
  249. package/src/shared/models/project/worker.ts +89 -90
  250. package/src/shared/resolvers/graph-resolver.ts +21 -0
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +9 -2
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +7 -3
  257. package/src/shared/utils/index.ts +1 -2
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +5 -23
  266. package/src/unlock/memory.ts +9 -14
  267. package/src/worker/abstractions.ts +7 -4
  268. package/src/worker/docker.ts +14 -19
  269. package/src/worker/manager.ts +366 -97
  270. package/dist/chunk-NAAIDR4U.js +0 -8499
  271. package/dist/chunk-NAAIDR4U.js.map +0 -1
  272. package/dist/chunk-Y7DXREVO.js +0 -1745
  273. package/dist/chunk-Y7DXREVO.js.map +0 -1
  274. package/dist/magic-string.es-5ABAC4JN.js +0 -1292
  275. package/dist/magic-string.es-5ABAC4JN.js.map +0 -1
  276. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +0 -356
  277. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +0 -274
  278. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +0 -223
  279. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +0 -147
  280. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +0 -280
  281. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +0 -360
  282. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +0 -215
  283. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +0 -427
  284. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +0 -217
  285. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +0 -132
  286. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +0 -454
  287. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +0 -426
  288. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +0 -372
  289. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +0 -383
  290. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +0 -245
  291. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +0 -174
  292. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +0 -432
  293. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +0 -220
  294. package/src/business/backend-unlock.ts +0 -10
  295. package/src/common/clock.ts +0 -18
  296. package/src/common/performance.ts +0 -44
  297. package/src/common/random.ts +0 -68
  298. package/src/common/test/index.ts +0 -2
  299. package/src/common/test/render.ts +0 -98
  300. package/src/common/test/tracer.ts +0 -359
  301. package/src/hotstate/abstractions.ts +0 -48
  302. package/src/hotstate/factory.ts +0 -17
  303. package/src/hotstate/index.ts +0 -3
  304. package/src/hotstate/manager.ts +0 -192
  305. package/src/hotstate/memory.ts +0 -100
  306. package/src/hotstate/validation.ts +0 -100
  307. package/src/lock/test.ts +0 -108
  308. package/src/project/abstractions.ts +0 -78
  309. package/src/project/evaluation.ts +0 -248
  310. package/src/project/factory.ts +0 -11
  311. package/src/project/index.ts +0 -3
  312. package/src/project/local.ts +0 -417
  313. package/src/pubsub/local.ts +0 -36
  314. package/src/pubsub/validation.ts +0 -33
  315. package/src/shared/utils/args.ts +0 -25
  316. package/src/state/abstractions.ts +0 -289
  317. package/src/state/encryption.ts +0 -98
  318. package/src/state/factory.ts +0 -20
  319. package/src/state/index.ts +0 -7
  320. package/src/state/local/backend.ts +0 -106
  321. package/src/state/local/collection.ts +0 -361
  322. package/src/state/local/index.ts +0 -2
  323. package/src/state/manager.ts +0 -890
  324. package/src/state/memory/backend.ts +0 -70
  325. package/src/state/memory/collection.ts +0 -270
  326. package/src/state/memory/index.ts +0 -2
  327. package/src/state/repository/index.ts +0 -2
  328. package/src/state/repository/repository.index.ts +0 -193
  329. package/src/state/repository/repository.ts +0 -507
  330. package/src/state/test.ts +0 -457
  331. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -1,434 +1,679 @@
1
- import type { HotStateManager } from "../hotstate"
2
- import type { LockManager } from "../lock"
3
- import type { PubSubManager } from "../pubsub"
4
- import type { StateManager } from "../state"
1
+ import type {
2
+ ComponentKind,
3
+ UnitPage,
4
+ UnitTerminal,
5
+ UnitTrigger,
6
+ UnitWorker,
7
+ } from "@highstate/contract"
5
8
  import type { Logger } from "pino"
6
- import type { RunnerBackend } from "../runner"
7
9
  import type { ArtifactService } from "../artifact"
8
- import { randomBytes } from "node:crypto"
9
- import { parseInstanceId } from "@highstate/contract"
10
+ import type { SecretService, UnitExtraService, WorkerService } from "../business"
11
+ import type { PubSubManager } from "../pubsub"
12
+ import type { RunnerBackend } from "../runner"
13
+ import { type InstanceId, parseInstanceId } from "@highstate/contract"
14
+ import { isNonNullish, omit } from "remeda"
15
+ import {
16
+ type DatabaseManager,
17
+ DbNull,
18
+ type InstanceOperationStateCreateManyInput,
19
+ type InstanceOperationStateUpdateInput,
20
+ type InstanceStateInclude,
21
+ type InstanceStateUpdateInput,
22
+ type ProjectTransaction,
23
+ } from "../database"
10
24
  import {
11
- applyStatePatch,
12
- isStableOperationStatus,
13
- isStateEmpty,
14
- type InstanceCustomStatus,
25
+ forSchema,
26
+ type InstanceCustomStatusInput,
27
+ InstanceLockedError,
28
+ type InstanceOperationState,
15
29
  type InstanceState,
16
- type InstanceStatePatch,
17
- type Operation,
30
+ InstanceStateNotFoundError,
31
+ ProjectNotFoundError,
32
+ projectOutputSchema,
33
+ waitAll,
18
34
  } from "../shared"
19
35
 
20
- export class InstanceStateService {
21
- constructor(
22
- private readonly stateManager: StateManager,
23
- private readonly hotStateManager: HotStateManager,
24
- private readonly pubsubManager: PubSubManager,
25
- private readonly lockManager: LockManager,
26
- private readonly runnerBackend: RunnerBackend,
27
- private readonly artifactManager: ArtifactService,
28
- private readonly logger: Logger,
29
- ) {}
36
+ export type GetProjectInstancesOptions = {
37
+ /**
38
+ * Whether to include evaluation states in the result.
39
+ *
40
+ * By default, this is false.
41
+ */
42
+ includeEvaluationState?: boolean
30
43
 
31
44
  /**
32
- * Gets the current states of all instances in a project.
45
+ * Include last operation state in the result.
33
46
  *
34
- * @param projectId The ID of the project for which to retrieve instance states.
35
- * @param checkLoaded Whether to check if instance states are loaded in the hot state.
36
- * Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
47
+ * By default, this is false.
37
48
  */
38
- async getInstanceStates(projectId: string, checkLoaded = true): Promise<InstanceState[]> {
39
- return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
40
- const entries = await this.hotStateManager.hgetall(["instance-states", projectId])
49
+ includeLastOperationState?: boolean
41
50
 
42
- return entries.map(([, value]) => value)
43
- })
44
- }
51
+ /**
52
+ * Include the instance ID of the parent instance in the result.
53
+ *
54
+ * By default, this is false.
55
+ */
56
+ includeParentInstanceId?: boolean
45
57
 
46
58
  /**
47
- * Gets the state of a specific instance in a project.
59
+ * Whether to include `terminalIds` and `pageIds` in the result.
48
60
  *
49
- * @param projectId The ID of the project to which the instance belongs.
50
- * @param instanceId The ID of the instance to retrieve the state for.
51
- * @param checkLoaded Whether to check if instance states are loaded in the hot state.
52
- * Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
61
+ * By default, this is false.
53
62
  */
54
- async getInstanceState(
55
- projectId: string,
56
- instanceId: string,
57
- checkLoaded = true,
58
- ): Promise<InstanceState | null> {
59
- return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
60
- return await this.hotStateManager.hget(["instance-states", projectId], instanceId)
61
- })
62
- }
63
+ includeExtra?: boolean
63
64
 
64
65
  /**
65
- * Updates the states of multiple instances in a project.
66
+ * Whether to load custom statuses for each instance.
66
67
  *
67
- * @param projectId The ID of the project to which the instances belong.
68
- * @param states The array of instance states to update.
69
- * @param persistent Whether to persist the changes in the state backend.
70
- * @param checkLoaded Whether to check if instance states are loaded in the hot state.
71
- * Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
68
+ * By default, this is false.
72
69
  */
73
- async updateInstanceStates(
74
- projectId: string,
75
- states: InstanceState[],
76
- persistent = false,
77
- checkLoaded = true,
78
- ): Promise<InstanceState[]> {
79
- return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
80
- const batch = this.stateManager.batch()
81
- const promises: Promise<void>[] = []
82
-
83
- for (const state of states) {
84
- promises.push(
85
- this.hotStateManager.hset(["instance-states", projectId], state.id, state),
86
- this.pubsubManager.publish(["instance-state", projectId], { type: "updated", state }),
87
- )
70
+ loadCustomStatuses?: boolean
71
+ }
88
72
 
89
- if (persistent) {
90
- promises.push(
91
- this.stateManager.getInstanceStateRepository(projectId).putItem(state, batch),
92
- )
93
- }
94
- }
73
+ export type ForgetInstanceStateOptions = {
74
+ /**
75
+ * Whether to delete terminals and their sessions associated with the instance.
76
+ *
77
+ * If `false`, the terminals will be marked as deleted and no new sessions will be allowed to be created,
78
+ * but existing sessions will remain to provide history and logs.
79
+ *
80
+ * By default, this is false.
81
+ */
82
+ clearTerminalData?: boolean
95
83
 
96
- await Promise.all(promises)
97
- await batch.write()
84
+ /**
85
+ * Whether to delete the secrets associated with the instance.
86
+ *
87
+ * By default, this is false.
88
+ */
89
+ deleteSecrets?: boolean
90
+ }
98
91
 
99
- this.logger.debug(
100
- { projectId, states },
101
- `instance states updated for project "%s"`,
102
- projectId,
103
- )
92
+ export type InstanceStatePatch = Pick<
93
+ Partial<InstanceState>,
94
+ | "status"
95
+ | "message"
96
+ | "statusFields"
97
+ | "parentId"
98
+ | "lastOperationState"
99
+ | "inputHash"
100
+ | "outputHash"
101
+ | "dependencyOutputHash"
102
+ | "model"
103
+ | "resolvedInputs"
104
+ | "currentResourceCount"
105
+ >
106
+
107
+ export type UpdateOperationStateOptions = {
108
+ /**
109
+ * The operation state to update.
110
+ */
111
+ operationState: InstanceOperationStateUpdateInput
104
112
 
105
- return states
106
- })
107
- }
113
+ /**
114
+ * Instance state patch to update or function to compute patch from current state.
115
+ */
116
+ instanceState?: InstanceStatePatch
108
117
 
109
118
  /**
110
- * Patches multiple instance states in a project.
111
- *
112
- * @param projectId The ID of the project to which the instances belong.
113
- * @param patches The array of instance state patches to apply.
114
- * @param checkLoaded Whether to check if instance states are loaded in the hot state.
115
- * Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
119
+ * Unit-specific extra data to update.
116
120
  */
117
- async patchInstanceStates(
118
- projectId: string,
119
- patches: InstanceStatePatch[],
120
- checkLoaded = true,
121
- ): Promise<InstanceState[]> {
122
- return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
123
- const batch = this.stateManager.batch()
124
- const promises: Promise<InstanceState>[] = []
125
-
126
- for (const patch of patches) {
127
- promises.push(
128
- this.hotStateManager.hget(["instance-states", projectId], patch.id).then(state => {
129
- return this.applyStatePatch(projectId, state, patch, true)
130
- }),
131
- )
132
- }
121
+ unitExtra?: {
122
+ /**
123
+ * Unit pages to update.
124
+ */
125
+ pages: UnitPage[]
126
+
127
+ /**
128
+ * Unit terminals to update.
129
+ */
130
+ terminals: UnitTerminal[]
131
+
132
+ /**
133
+ * Unit triggers to update.
134
+ */
135
+ triggers: UnitTrigger[]
136
+
137
+ /**
138
+ * Unit workers to update.
139
+ */
140
+ workers: UnitWorker[]
141
+
142
+ /**
143
+ * Unit secrets to update.
144
+ */
145
+ secrets: Record<string, unknown>
146
+ }
147
+ }
133
148
 
134
- const updatedStates = await Promise.all(promises)
149
+ export function includeForInstanceState(
150
+ options: GetProjectInstancesOptions = {},
151
+ ): InstanceStateInclude {
152
+ return {
153
+ secrets: {
154
+ select: { name: true },
155
+ },
135
156
 
136
- await batch.write()
157
+ parent: options.includeParentInstanceId ? { select: { instanceId: true } } : undefined,
137
158
 
138
- this.logger.debug(
139
- { projectId, patches },
140
- `instance states patched for project "%s"`,
141
- projectId,
142
- )
159
+ operationStates: options.includeLastOperationState
160
+ ? { take: 1, orderBy: { startedAt: "desc" } }
161
+ : undefined,
143
162
 
144
- return updatedStates
145
- })
146
- }
163
+ evaluationState: options.includeEvaluationState,
147
164
 
148
- /**
149
- * Patches an instance state in a project.
150
- * TODO: delete
151
- *
152
- * @param projectId The ID of the project to which the instance belongs.
153
- * @param patch The patch to apply to the instance state.
154
- * @param checkLoaded Whether to check if instance states are loaded in the hot state.
155
- * Can be set to false if you are sure that the states are already loaded (e.g., if updating states within operation).
156
- */
157
- async patchInstanceState(
158
- projectId: string,
159
- patch: InstanceStatePatch,
160
- checkLoaded = true,
161
- ): Promise<InstanceState> {
162
- return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
163
- return await this.lockManager.acquire(["instance-state", projectId, patch.id], async () => {
164
- const state = await this.hotStateManager.hget(["instance-states", projectId], patch.id)
165
-
166
- const patchedState = await this.applyStatePatch(
167
- projectId,
168
- state,
169
- patch,
170
- patch.operationStatus !== undefined,
171
- )
165
+ terminals: options.includeExtra ? { select: { id: true } } : undefined,
166
+ pages: options.includeExtra ? { select: { id: true } } : undefined,
172
167
 
173
- this.logger.debug(
174
- { projectId, instanceId: patch.id, patch },
175
- `instance state updated for instance "%s" in project "%s"`,
176
- patch.id,
177
- projectId,
178
- )
168
+ customStatuses: options.loadCustomStatuses
169
+ ? {
170
+ orderBy: [{ order: "asc" }, { createdAt: "asc" }],
171
+ }
172
+ : undefined,
173
+ }
174
+ }
179
175
 
180
- return patchedState
181
- })
182
- })
176
+ export function mapInstanceStateResult(
177
+ instance: InstanceState & {
178
+ secrets: { name: string | null }[]
179
+ parent?: { instanceId: InstanceId } | null
180
+ operationStates?: InstanceOperationState[]
181
+ terminals?: { id: string }[]
182
+ pages?: { id: string }[]
183
+ customStatuses: InstanceState["customStatuses"]
184
+ },
185
+ ): InstanceState {
186
+ return {
187
+ ...omit(instance, [
188
+ "secrets",
189
+ "parent",
190
+ "operationStates",
191
+ "terminals",
192
+ "pages",
193
+ "customStatuses",
194
+ ]),
195
+
196
+ secretNames: instance.secrets.map(secret => secret.name).filter(isNonNullish),
197
+ parentInstanceId: instance.parent ? (instance.parent?.instanceId ?? null) : undefined,
198
+ lastOperationState: instance.operationStates?.[0],
199
+ evaluationState: instance.evaluationState ?? undefined,
200
+ terminalIds: instance.terminals ? instance.terminals.map(terminal => terminal.id) : undefined,
201
+ pageIds: instance.pages ? instance.pages.map(page => page.id) : undefined,
202
+ customStatuses: instance.customStatuses ?? undefined,
183
203
  }
204
+ }
205
+
206
+ export class InstanceStateService {
207
+ constructor(
208
+ private readonly database: DatabaseManager,
209
+ private readonly pubsubManager: PubSubManager,
210
+ private readonly runnerBackend: RunnerBackend,
211
+ private readonly workerService: WorkerService,
212
+ private readonly artifactService: ArtifactService,
213
+ private readonly unitExtraService: UnitExtraService,
214
+ private readonly secretService: SecretService,
215
+ private readonly logger: Logger,
216
+ ) {}
184
217
 
185
218
  /**
186
- * Patches an instance state within an operation.
219
+ * Gets the aggregates of all instances in the project.
187
220
  *
188
- * @param projectId The ID of the project to which the instance belongs.
189
- * @param operation The operation context for which the instance state is being patched.
190
- * @param patch The patch to apply to the instance state.
221
+ * @param projectId The ID of the project for which to retrieve instances.
222
+ * @param options Options to customize the retrieval of instances.
191
223
  */
192
- async patchOperationInstanceState(
224
+ async getInstanceStates(
193
225
  projectId: string,
194
- operation: Operation,
195
- patch: InstanceStatePatch,
196
- ): Promise<InstanceState> {
197
- return await this.lockManager.acquire(["instance-state", projectId, patch.id], async () => {
198
- const state = await this.hotStateManager.hget(["instance-states", projectId], patch.id)
199
-
200
- const patchedState = await this.applyStatePatch(
201
- projectId,
202
- state,
203
- patch,
204
- operation.type !== "preview",
205
- )
206
-
207
- this.logger.debug(
208
- { projectId, instanceId: patch.id, patch },
209
- `instance state updated for instance "%s" in project "%s"`,
210
- patch.id,
211
- projectId,
212
- )
226
+ options: GetProjectInstancesOptions = {},
227
+ ): Promise<InstanceState[]> {
228
+ const database = await this.database.forProject(projectId)
213
229
 
214
- return patchedState
230
+ // load instance states from the database
231
+ const queryResult = await database.instanceState.findMany({
232
+ include: includeForInstanceState(options),
215
233
  })
234
+
235
+ // aggregate the results from the database
236
+ return queryResult.map(mapInstanceStateResult)
216
237
  }
217
238
 
218
239
  /**
219
- * Updates the secret names of an instance state.
240
+ * Marks an instance state as deleted and cleans up associated resources.
241
+ *
242
+ * This operation:
243
+ * - only allows deletion of instances with operation state null or "destroyed";
244
+ * - prevents deletion of instances that have active locks;
245
+ * - marks the instance state as "deleted" (never actually removes the record);
246
+ * - handles terminals: deletes data if requested, otherwise marks as "unavailable";
247
+ * - handles secrets: deletes if requested, otherwise ignores them;
248
+ * - deletes artifact references for the instance;
249
+ * - recursively handles child instances using parentId relationship;
250
+ * - performs worker cleanup and synchronization;
251
+ * - performs artifact garbage collection.
220
252
  *
221
- * @param projectId The ID of the project to which the instance belongs.
222
- * @param instanceId The ID of the instance whose state is to be updated.
223
- * @param secretNamesToAdd The list of secret names to add to the instance state.
224
- * @param secretNamesToRemove The list of secret names to remove from the instance state.
225
- * @param invalidateState Whether to invalidate the state after updating secret names.
226
- * @param checkLoaded Whether to check if instance states are loaded in the hot state.
253
+ * @param projectId The ID of the project containing the instance.
254
+ * @param instanceId The ID of the instance whose state is to be marked as deleted.
255
+ * @param options Configuration options for terminal and secret handling.
227
256
  */
228
- async updateStateSecretNames(
257
+ async forgetInstanceState(
229
258
  projectId: string,
230
- instanceId: string,
231
- secretNamesToAdd: string[],
232
- secretNamesToRemove: string[],
233
- invalidateState = true,
234
- checkLoaded = true,
259
+ instanceId: InstanceId,
260
+ { deleteSecrets = false, clearTerminalData = false }: ForgetInstanceStateOptions = {},
235
261
  ): Promise<void> {
236
- return await this.withLoadedInstanceStates(projectId, checkLoaded, async () => {
237
- await this.lockManager.acquire(["instance-state", projectId, instanceId], async () => {
238
- const state = await this.hotStateManager.hget(["instance-states", projectId], instanceId)
262
+ const database = await this.database.forProject(projectId)
263
+ const project = await this.database.backend.project.findUnique({
264
+ where: { id: projectId },
265
+ select: forSchema(projectOutputSchema),
266
+ })
239
267
 
240
- const secretNameSet = new Set(state?.secretNames ?? [])
241
- for (const name of secretNamesToAdd) {
242
- secretNameSet.add(name)
243
- }
268
+ if (!project) {
269
+ throw new ProjectNotFoundError(projectId)
270
+ }
244
271
 
245
- for (const name of secretNamesToRemove) {
246
- secretNameSet.delete(name)
247
- }
272
+ // collect instances to process cleanup after transaction
273
+ const unitInstancesToCleanup: { id: string; instanceId: InstanceId }[] = []
274
+ const updatedStateIds: string[] = []
248
275
 
249
- const patch: InstanceStatePatch = {
250
- id: instanceId,
251
- secretNames: Array.from(secretNameSet),
252
- }
276
+ await database.$transaction(async tx => {
277
+ const state = await tx.instanceState.findUnique({
278
+ where: { instanceId },
279
+ select: { id: true, kind: true, instanceId: true, lock: { select: { stateId: true } } },
280
+ })
253
281
 
254
- if (state?.operationStatus && invalidateState) {
255
- // invalidate the input hash nonce if the operation status is present
256
- // if not, the invalidation makes no sense
257
- patch.operationStatus = {
258
- inputHashNonce: randomBytes(4).readInt32LE(),
259
- }
260
- }
282
+ if (!state) {
283
+ throw new InstanceStateNotFoundError(projectId, instanceId)
284
+ }
261
285
 
262
- await this.applyStatePatch(projectId, state, patch, true)
263
- })
286
+ if (state.lock) {
287
+ throw new InstanceLockedError(projectId, instanceId)
288
+ }
289
+
290
+ await this.processInstanceDeletion(
291
+ tx,
292
+ projectId,
293
+ state,
294
+ { deleteSecrets, clearTerminalData },
295
+ unitInstancesToCleanup,
296
+ updatedStateIds,
297
+ )
264
298
  })
299
+
300
+ // publish state events for all updated instances
301
+ for (const updatedStateId of updatedStateIds) {
302
+ void this.pubsubManager.publish(["instance-state", projectId], {
303
+ type: "patched",
304
+ stateId: updatedStateId,
305
+ patch: {
306
+ status: "undeployed",
307
+ statusFields: null,
308
+ inputHash: null,
309
+ outputHash: null,
310
+ dependencyOutputHash: null,
311
+ message: null,
312
+ currentResourceCount: null,
313
+ model: null,
314
+ resolvedInputs: null,
315
+ secretNames: deleteSecrets ? [] : undefined,
316
+ terminalIds: clearTerminalData ? [] : undefined,
317
+ pageIds: [],
318
+ customStatuses: [],
319
+ triggerIds: [],
320
+ evaluationState: null,
321
+ },
322
+ })
323
+ }
324
+
325
+ // process side effects
326
+ try {
327
+ await waitAll([
328
+ this.workerService.cleanupWorkerUsageAndSync(projectId),
329
+ this.artifactService.collectGarbage(projectId),
330
+ ...unitInstancesToCleanup.map(async ({ id, instanceId }) => {
331
+ const [instanceType, instanceName] = parseInstanceId(instanceId)
332
+
333
+ await this.runnerBackend.deleteState({
334
+ projectId: project.id,
335
+ stateId: id,
336
+ libraryId: project.libraryId,
337
+ instanceName,
338
+ instanceType,
339
+ })
340
+ }),
341
+ ])
342
+ } catch (error) {
343
+ this.logger.warn(
344
+ { error, projectId, instanceId },
345
+ "failed to perform side effects after forgetting instance state",
346
+ )
347
+ }
265
348
  }
266
349
 
267
350
  /**
268
- * Completely deletes the instance state from the system including all related resources.
269
- *
270
- * @param projectId The ID of the project from which to delete the instance state.
271
- * @param instanceId The ID of the instance whose state is to be deleted.
351
+ * Processes the deletion of an instance within a transaction.
352
+ * Handles validation, deletion logic, and recursive child deletion.
272
353
  */
273
- async deleteInstanceState(projectId: string, instanceId: string): Promise<void> {
274
- await this.lockManager.acquire(["instance-state", projectId, instanceId], async () => {
275
- const [instanceType, instanceName] = parseInstanceId(instanceId)
276
- const state = await this.getInstanceState(projectId, instanceId)
277
-
278
- const batch = this.stateManager.batch()
354
+ private async processInstanceDeletion(
355
+ tx: ProjectTransaction,
356
+ projectId: string,
357
+ state: { id: string; kind: ComponentKind; instanceId: InstanceId },
358
+ options: ForgetInstanceStateOptions,
359
+ unitInstancesToCleanup: { id: string; instanceId: InstanceId }[],
360
+ updatedStateIds: string[],
361
+ ): Promise<void> {
362
+ const { deleteSecrets = false, clearTerminalData = false } = options
363
+
364
+ // always mark instance state as undeployed (never actually delete the record)
365
+ await tx.instanceState.update({
366
+ where: { id: state.id },
367
+ data: {
368
+ status: "undeployed",
369
+ statusFields: DbNull,
370
+ inputHash: null,
371
+ outputHash: null,
372
+ dependencyOutputHash: null,
373
+ message: null,
374
+ currentResourceCount: null,
375
+ model: DbNull,
376
+ resolvedInputs: DbNull,
377
+ },
378
+ })
279
379
 
280
- await Promise.allSettled([
281
- // remove from the state
282
- this.stateManager.getInstanceStateRepository(projectId).delete(instanceId, batch),
380
+ // handle terminals
381
+ if (clearTerminalData) {
382
+ await tx.terminal.deleteMany({ where: { stateId: state.id } })
383
+ } else {
384
+ await tx.terminal.updateMany({
385
+ where: { stateId: state.id },
386
+ data: { status: "unavailable" },
387
+ })
388
+ }
283
389
 
284
- // clear related resources
285
- this.stateManager.getInstanceLockRepository(projectId).delete(instanceId, batch),
390
+ // handle secrets
391
+ if (deleteSecrets) {
392
+ await tx.secret.deleteMany({
393
+ where: { stateId: state.id },
394
+ })
395
+ }
286
396
 
287
- this.stateManager
288
- .getPageRepository(projectId)
289
- .deleteMany(Object.values(state?.extra?.pageIds ?? {}), batch),
397
+ // delete custom statuses for this instance
398
+ await tx.instanceCustomStatus.deleteMany({
399
+ where: { stateId: state.id },
400
+ })
290
401
 
291
- this.stateManager
292
- .getTriggerRepository(projectId)
293
- .deleteMany(Object.values(state?.extra?.triggerIds ?? {}), batch),
402
+ // delete other related resources
403
+ await tx.page.deleteMany({ where: { stateId: state.id } })
404
+ await tx.trigger.deleteMany({ where: { stateId: state.id } })
405
+
406
+ // remove artifact references for this instance
407
+ await tx.instanceState.update({
408
+ where: { id: state.id },
409
+ data: {
410
+ artifacts: {
411
+ set: [],
412
+ },
413
+ },
414
+ })
294
415
 
295
- this.stateManager
296
- .getTerminalRepository(projectId)
297
- .deleteMany(Object.values(state?.extra?.terminalIds ?? {}), batch),
298
- ])
416
+ // collect unit instances for Pulumi cleanup (to be done outside transaction)
417
+ if (state.kind === "unit") {
418
+ unitInstancesToCleanup.push({ id: state.id, instanceId: state.instanceId })
419
+ }
299
420
 
300
- await Promise.allSettled([
301
- // commit the state batch
302
- batch.write(),
421
+ // track this instance as updated
422
+ updatedStateIds.push(state.id)
303
423
 
304
- // update hotstate
305
- this.hotStateManager.hdel(["instance-states", projectId], instanceId),
424
+ this.logger.info({ projectId }, `marked state "%s" as deleted`, state.id)
306
425
 
307
- // publish the instance state deletion event
308
- this.pubsubManager.publish(["instance-state", projectId], { type: "deleted", instanceId }),
426
+ // recursively handle child instances using parentId
427
+ if (state.kind === "composite") {
428
+ const childStates = await tx.instanceState.findMany({
429
+ where: {
430
+ parentId: state.id,
431
+ status: { not: "undeployed" }, // don't process undeployed children
432
+ },
433
+ select: { id: true, kind: true, instanceId: true },
434
+ })
309
435
 
310
- // remove all artifact references
311
- this.artifactManager.removeUsages(
436
+ // recursively delete child states (within the same transaction)
437
+ for (const child of childStates) {
438
+ await this.processInstanceDeletion(
439
+ tx,
312
440
  projectId,
313
- Object.values(state?.extra?.exportedArtifactIds ?? {}).flat(),
314
- [{ type: "instance", instanceId }],
315
- ),
316
-
317
- // clear pulumi state
318
- this.runnerBackend.deleteState({ projectId, instanceName, instanceType }),
319
- ])
320
- })
441
+ child,
442
+ options,
443
+ unitInstancesToCleanup,
444
+ updatedStateIds,
445
+ )
446
+ }
447
+ }
321
448
  }
322
449
 
450
+ /**
451
+ * Replaces or adds a custom status for an instance in a project.
452
+ *
453
+ * @param projectId The ID of the project containing the instance.
454
+ * @param serviceAccoundtId The ID of the service account owning the instance.
455
+ * @param stateId The ID of the instance state to update.
456
+ * @param status The custom status to replace or add.
457
+ */
323
458
  async updateCustomStatus(
324
459
  projectId: string,
325
- instanceId: string,
326
- status: InstanceCustomStatus,
460
+ stateId: string,
461
+ serviceAccountId: string,
462
+ status: InstanceCustomStatusInput,
327
463
  ): Promise<void> {
328
- await this.lockManager.acquire(["instance-state", projectId, instanceId], async () => {
329
- const state = await this.hotStateManager.hget(["instance-states", projectId], instanceId)
330
- if (!state) {
331
- throw new Error(
332
- `Instance state with ID "${instanceId}" not found in project "${projectId}"`,
333
- )
334
- }
464
+ const database = await this.database.forProject(projectId)
465
+
466
+ const customStatuses = await database.$transaction(async tx => {
467
+ await tx.instanceCustomStatus.upsert({
468
+ where: {
469
+ stateId_serviceAccountId_name: {
470
+ stateId,
471
+ serviceAccountId,
472
+ name: status.name,
473
+ },
474
+ },
475
+ create: {
476
+ stateId: stateId,
477
+ serviceAccountId,
478
+ name: status.name,
479
+ meta: status.meta,
480
+ value: status.value,
481
+ message: status.message ?? null,
482
+ order: status.order ?? 50,
483
+ },
484
+ update: {
485
+ meta: status.meta,
486
+ value: status.value,
487
+ message: status.message ?? null,
488
+ order: status.order ?? 50,
489
+ },
490
+ })
335
491
 
336
- const existingStatus = state.extra?.customStatuses?.find(s => s.name === status.name)
337
- if (existingStatus) {
338
- Object.assign(existingStatus, status)
339
- } else {
340
- state.extra ??= {}
341
- state.extra.customStatuses ??= []
342
- state.extra.customStatuses?.push(status)
343
- }
492
+ return await tx.instanceCustomStatus.findMany({
493
+ where: { stateId, serviceAccountId },
494
+ orderBy: [{ order: "asc" }, { createdAt: "asc" }],
495
+ })
496
+ })
344
497
 
345
- await this.applyStatePatch(projectId, state, { id: instanceId, extra: state.extra })
498
+ void this.pubsubManager.publish(["instance-state", projectId], {
499
+ type: "patched",
500
+ stateId,
501
+ patch: { customStatuses },
346
502
  })
347
503
  }
348
504
 
349
- private async withLoadedInstanceStates<T>(
505
+ /**
506
+ * Removes a custom status from an instance in a project.
507
+ *
508
+ * @param projectId The ID of the project containing the instance.
509
+ * @param stateId The ID of the instance state to update.
510
+ * @param serviceAccountId The ID of the service account owning the instance.
511
+ * @param statusName The name of the custom status to remove.
512
+ */
513
+ async removeCustomStatus(
350
514
  projectId: string,
351
- checkLoaded: boolean,
352
- action: () => Promise<T>,
353
- ): Promise<T> {
354
- if (!checkLoaded) {
355
- return await action()
356
- }
357
-
358
- return await this.lockManager.acquire(["project-instance-states", projectId], async () => {
359
- const instanceStatesLoaded = await this.hotStateManager.get([
360
- "instance-states-loaded",
361
- projectId,
362
- ])
515
+ stateId: string,
516
+ serviceAccountId: string,
517
+ statusName: string,
518
+ ): Promise<void> {
519
+ const database = await this.database.forProject(projectId)
520
+
521
+ const customStatuses = await database.$transaction(async tx => {
522
+ await tx.instanceCustomStatus.deleteMany({
523
+ where: {
524
+ stateId,
525
+ serviceAccountId,
526
+ name: statusName,
527
+ },
528
+ })
363
529
 
364
- if (instanceStatesLoaded) {
365
- this.logger.debug(`instance states already loaded for project "%s"`, projectId)
366
- return await action()
367
- }
530
+ return await tx.instanceCustomStatus.findMany({
531
+ where: { stateId, serviceAccountId },
532
+ orderBy: [{ order: "asc" }, { createdAt: "asc" }],
533
+ })
534
+ })
368
535
 
369
- // load instance states from the storage
370
- const states = await this.stateManager.getInstanceStateRepository(projectId).getAllItems()
371
- // store them in the hot state
372
- await this.hotStateManager.hmset(
373
- ["instance-states", projectId],
374
- states.map(state => [state.id, state]),
375
- )
536
+ void this.pubsubManager.publish(["instance-state", projectId], {
537
+ type: "patched",
538
+ stateId,
539
+ patch: { customStatuses },
540
+ })
541
+ }
376
542
 
377
- this.logger.info(
378
- { projectId, count: states.length },
379
- `loaded %d instance states`,
380
- states.length,
543
+ /**
544
+ * Creates the provided operation states.
545
+ * Also updates the instance state if provided.
546
+ *
547
+ * @param projectId The ID of the project containing the instances.
548
+ * @param operationStates The tuples of operation state data to create and instance state patch to apply.
549
+ */
550
+ async createOperationStates(
551
+ projectId: string,
552
+ operationStates: [
553
+ opState: InstanceOperationStateCreateManyInput,
554
+ instanceState: InstanceStatePatch,
555
+ ][],
556
+ ): Promise<Partial<InstanceState>[]> {
557
+ const database = await this.database.forProject(projectId)
558
+
559
+ const patches = await database.$transaction(async tx => {
560
+ return await Promise.all(
561
+ operationStates.map(async ([opState, instanceState]) => {
562
+ const operationState = await tx.instanceOperationState.create({
563
+ data: opState,
564
+ })
565
+
566
+ const state = await tx.instanceState.update({
567
+ where: { id: opState.stateId },
568
+ data: instanceState as InstanceStateUpdateInput,
569
+ })
570
+
571
+ return { ...state, lastOperationState: operationState }
572
+ }),
381
573
  )
574
+ })
382
575
 
383
- // mark instance states as loaded
384
- await this.hotStateManager.set(["instance-states-loaded", projectId], true)
576
+ // publish patches after transaction
577
+ for (const patch of patches) {
578
+ void this.pubsubManager.publish(["instance-state", projectId], {
579
+ type: "patched",
580
+ stateId: patch.id,
581
+ patch,
582
+ })
583
+ }
385
584
 
386
- return await action()
387
- })
585
+ return patches
388
586
  }
389
587
 
390
- private async applyStatePatch(
588
+ /**
589
+ * Updates the operation state and instance state for a specific instance.
590
+ *
591
+ * @param projectId The ID of the project containing the instance.
592
+ * @param stateId The ID of the instance state to update.
593
+ * @param operationId The ID of the operation (required if operationState is provided).
594
+ * @param options Update options containing operation state, instance state, and unit-specific data.
595
+ * @return The instance state patch containing the updated operation state and instance state fields.
596
+ */
597
+ async updateOperationState(
391
598
  projectId: string,
392
- state: InstanceState | null,
393
- patch: InstanceStatePatch,
394
- persistent = true,
395
- ): Promise<InstanceState> {
396
- const patchedState = applyStatePatch(state, patch)
397
-
398
- const promises: Promise<void>[] = [
399
- this.hotStateManager.hset(["instance-states", projectId], patchedState.id, patchedState),
400
- ]
401
-
402
- const shouldPersist =
403
- persistent &&
404
- (!patchedState.operationStatus ||
405
- isStableOperationStatus(patchedState.operationStatus?.status))
406
-
407
- const repo = this.stateManager.getInstanceStateRepository(projectId)
408
-
409
- if (isStateEmpty(patchedState)) {
410
- promises.push(
411
- this.pubsubManager.publish(["instance-state", projectId], {
412
- type: "deleted",
413
- instanceId: patchedState.id,
414
- }),
415
- )
599
+ stateId: string,
600
+ operationId: string,
601
+ options: UpdateOperationStateOptions,
602
+ ): Promise<Partial<InstanceState>> {
603
+ const { operationState, instanceState, unitExtra } = options
604
+ const database = await this.database.forProject(projectId)
605
+
606
+ const result = await database.$transaction(async tx => {
607
+ let unitExtraData = null
608
+
609
+ // update operation state
610
+ const updatedOperationState = await tx.instanceOperationState.update({
611
+ where: {
612
+ operationId_stateId: {
613
+ operationId,
614
+ stateId,
615
+ },
616
+ },
617
+ data: operationState,
618
+ })
416
619
 
417
- if (shouldPersist) {
418
- promises.push(repo.delete(patchedState.id))
620
+ const project = await this.database.backend.project.findUnique({
621
+ where: { id: projectId },
622
+ select: { libraryId: true },
623
+ })
624
+
625
+ if (!project) {
626
+ throw new ProjectNotFoundError(projectId)
627
+ }
628
+
629
+ // update unit-specific data if provided
630
+ if (unitExtra) {
631
+ const [pageIds, terminalIds, triggerIds, secretNames] = await Promise.all([
632
+ this.unitExtraService.processUnitPages(tx, stateId, unitExtra.pages),
633
+ this.unitExtraService.processUnitTerminals(tx, stateId, unitExtra.terminals),
634
+ this.unitExtraService.processUnitTriggers(tx, stateId, unitExtra.triggers),
635
+ this.secretService.updateInstanceSecretsCore(
636
+ tx,
637
+ project.libraryId,
638
+ stateId,
639
+ unitExtra.secrets,
640
+ ),
641
+ this.workerService.updateUnitRegistrations(tx, projectId, stateId, unitExtra.workers),
642
+ ])
643
+
644
+ unitExtraData = { pageIds, terminalIds, triggerIds, secretNames }
419
645
  }
420
- } else {
421
- promises.push(
422
- this.pubsubManager.publish(["instance-state", projectId], { type: "patched", patch }),
423
- )
424
646
 
425
- if (shouldPersist) {
426
- promises.push(repo.putItem(patchedState))
647
+ // update instance state if provided
648
+ if (instanceState) {
649
+ await tx.instanceState.update({
650
+ where: { id: stateId },
651
+ data: instanceState as InstanceStateUpdateInput,
652
+ })
427
653
  }
654
+
655
+ return { updatedOperationState, unitExtraData }
656
+ })
657
+
658
+ // build patch combining operation state, instance state, and unit extra data
659
+ const patch: Partial<InstanceState> = {
660
+ ...instanceState,
661
+ ...result.unitExtraData,
662
+ lastOperationState: result.updatedOperationState,
428
663
  }
429
664
 
430
- await Promise.allSettled(promises)
665
+ // emit the patch after transaction
666
+ void this.pubsubManager.publish(["instance-state", projectId], {
667
+ type: "patched",
668
+ stateId,
669
+ patch,
670
+ })
671
+
672
+ this.logger.debug(
673
+ { projectId, stateId, operationId, options },
674
+ "updated operation state for instance",
675
+ )
431
676
 
432
- return patchedState
677
+ return patch
433
678
  }
434
679
  }