@highstate/backend 0.9.18 → 0.9.19

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-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/{chunk-OU5OQBLB.js → chunk-I7BWSAN6.js} +3 -28
  4. package/dist/{chunk-OU5OQBLB.js.map → chunk-I7BWSAN6.js.map} +1 -1
  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 +7587 -7291
  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 +27 -0
  32. package/prisma/project/artifact.prisma +52 -0
  33. package/prisma/project/custom-status.prisma +46 -0
  34. package/prisma/project/evaluation.prisma +35 -0
  35. package/prisma/project/instance.prisma +160 -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 +41 -0
  72. package/prisma/project/secret.prisma +42 -0
  73. package/prisma/project/service-account.prisma +36 -0
  74. package/prisma/project/terminal.prisma +90 -0
  75. package/prisma/project/trigger.prisma +31 -0
  76. package/prisma/project/unlock-method.prisma +32 -0
  77. package/prisma/project/worker.prisma +138 -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 +465 -130
  97. package/src/business/secret.ts +186 -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 +435 -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 +33 -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 +64 -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 +222 -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 +1 -1
  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,251 +1,217 @@
1
+ import type { InstanceId } from "@highstate/contract"
1
2
  import type { Logger } from "pino"
3
+ import type { DatabaseManager, Operation, OperationStatus, OperationUpdateInput } from "../database"
2
4
  import type { PubSubManager } from "../pubsub"
3
- import type { HotStateManager } from "../hotstate"
4
- import { v7 as uuidv7 } from "uuid"
5
- import { SAME_KEY, type StateManager } from "../state"
6
- import { isFinalOperationStatus, type Operation } from "../shared"
5
+ import { ulid } from "ulid"
6
+ import {
7
+ type OperationMeta,
8
+ OperationNotFoundError,
9
+ type OperationOptions,
10
+ type OperationType,
11
+ } from "../shared"
7
12
 
8
13
  export class OperationService {
9
14
  constructor(
10
- private readonly stateManager: StateManager,
11
- private readonly hotStateManager: HotStateManager,
15
+ private readonly database: DatabaseManager,
12
16
  private readonly pubsubManager: PubSubManager,
13
17
  private readonly logger: Logger,
14
18
  ) {}
15
19
 
16
- async getActiveOperaiton(projectId: string, operationId: string): Promise<Operation | null> {
17
- try {
18
- return await this.hotStateManager.hget(["active-operations", projectId], operationId)
19
- } catch (error) {
20
- this.logger.error(
21
- { projectId, operationId, error },
22
- `failed to get active operation with ID "%s" in project "%s" from hot state`,
23
- operationId,
24
- projectId,
25
- )
20
+ /**
21
+ * Creates a new operation in the database.
22
+ *
23
+ * @param projectId The project ID to which the operation belongs.
24
+ * @param meta The operation metadata.
25
+ * @param type The operation type.
26
+ * @param requestedInstanceIds The instance IDs that were explicitly requested.
27
+ * @param options The operation options.
28
+ * @returns The created operation.
29
+ */
30
+ async createOperation(
31
+ projectId: string,
32
+ meta: OperationMeta,
33
+ type: OperationType,
34
+ requestedInstanceIds: InstanceId[],
35
+ options: OperationOptions,
36
+ ): Promise<Operation> {
37
+ const database = await this.database.forProject(projectId)
38
+
39
+ const operation = await database.operation.create({
40
+ data: {
41
+ meta,
42
+ type,
43
+ options,
44
+ requestedInstanceIds,
45
+ startedAt: new Date(),
46
+ },
47
+ })
48
+
49
+ await this.pubsubManager.publish(["operation", projectId], {
50
+ type: "updated",
51
+ operation,
52
+ })
53
+
54
+ this.logger.info({ projectId, operationId: operation.id }, "created operation")
55
+ return operation
56
+ }
26
57
 
27
- return null
28
- }
58
+ /**
59
+ * Updates an existing operation in the database.
60
+ *
61
+ * @param projectId The project ID containing the operation.
62
+ * @param operationId The operation ID to update.
63
+ * @param updates The updates to apply.
64
+ * @returns The updated operation.
65
+ */
66
+ async updateOperation(
67
+ projectId: string,
68
+ operationId: string,
69
+ updates: OperationUpdateInput,
70
+ ): Promise<Operation> {
71
+ const database = await this.database.forProject(projectId)
72
+
73
+ const operation = await database.operation.update({
74
+ where: { id: operationId },
75
+ data: updates,
76
+ })
77
+
78
+ await this.pubsubManager.publish(["operation", projectId], {
79
+ type: "updated",
80
+ operation,
81
+ })
82
+
83
+ this.logger.info({ projectId, operationId }, "updated operation")
84
+ return operation
29
85
  }
30
86
 
31
87
  /**
32
- * Persists an operation to the state and publishes an update event.
88
+ * Gets an operation by ID.
33
89
  *
34
- * @param projectId The project ID to which the operation belongs.
35
- * @param operation The operation to persist.
90
+ * @param projectId The project ID containing the operation.
91
+ * @param operationId The operation ID.
92
+ * @returns The operation or undefined if not found.
36
93
  */
37
- async updateOperation(projectId: string, operation: Operation): Promise<void> {
38
- // 1. update the operation in the hot state
39
- try {
40
- if (isFinalOperationStatus(operation.status)) {
41
- await this.hotStateManager.hdel(["active-operations", projectId], operation.id)
42
- } else {
43
- await this.hotStateManager.hset(["active-operations", projectId], operation.id, operation)
44
- }
45
- } catch (error) {
46
- this.logger.error(
47
- { projectId, operationId: operation.id, error },
48
- `failed to update operation with ID "%s" in project "%s" in hot state`,
49
- operation.id,
50
- projectId,
51
- )
52
- }
94
+ async getOperation(projectId: string, operationId: string): Promise<Operation | undefined> {
95
+ const database = await this.database.forProject(projectId)
53
96
 
54
- // 2. persist the operation in the state repository
55
- try {
56
- await using batch = this.stateManager.batch()
57
-
58
- // persist the operation to the operation repository
59
- await this.stateManager.getOperationRepository(projectId).put(operation.id, operation, batch)
60
-
61
- if (operation.status === "pending") {
62
- // add the just created operation to the active operations index
63
- await this.stateManager
64
- .getActiveOperationIndexRepository(projectId)
65
- .indexRepository.put(operation.id, operation.id, batch)
66
- } else if (isFinalOperationStatus(operation.status)) {
67
- // remove the finished operation from the active operations index
68
- await this.stateManager
69
- .getActiveOperationIndexRepository(projectId)
70
- .indexRepository.delete(operation.id, batch)
71
- }
72
-
73
- await batch.write()
74
-
75
- try {
76
- await this.pubsubManager.publish(["operation", operation.id], {
77
- type: "updated",
78
- operation,
79
- })
80
- } catch (error) {
81
- this.logger.error(
82
- { projectId, operationId: operation.id, error },
83
- `failed to publish operation update for operation with ID "%s" in project "%s"`,
84
- operation.id,
85
- projectId,
86
- )
87
- }
88
-
89
- this.logger.info(
90
- { projectId, operationId: operation.id },
91
- `updated operation with ID "%s" in project "%s"`,
92
- operation.id,
93
- projectId,
94
- )
95
- } catch (error) {
96
- this.logger.error(
97
- { projectId, operationId: operation.id, error },
98
- `failed to update operation with ID "%s" in project "%s"`,
99
- operation.id,
100
- projectId,
101
- )
102
-
103
- throw new Error(`Failed to update operation "${operation.id}" in project "${projectId}"`, {
104
- cause: error,
105
- })
106
- }
97
+ const operation = await database.operation.findUnique({
98
+ where: { id: operationId },
99
+ })
100
+
101
+ return operation ?? undefined
102
+ }
103
+
104
+ /**
105
+ * Gets all operations for a project.
106
+ *
107
+ * @param projectId The project ID.
108
+ * @param limit Optional limit on number of operations to return.
109
+ * @returns Array of operations.
110
+ */
111
+ async getOperations(projectId: string, limit?: number): Promise<Operation[]> {
112
+ const database = await this.database.forProject(projectId)
113
+
114
+ return await database.operation.findMany({
115
+ orderBy: { startedAt: "desc" },
116
+ take: limit,
117
+ })
107
118
  }
108
119
 
109
120
  /**
110
- * Appends logs for a specific operation.
111
- * Creates all necessary indexes to retrieve the logs by instance ID.
121
+ * Appends log for a specific operation.
112
122
  *
113
- * @param projectId The ID of the project to persist the logs for.
114
- * @param operationId The ID of the operation to persist the logs for.
115
- * @param instanceIds The IDs of the instances to persist the logs for. Should include the whole hierarchy of instances produced the logs.
116
- * @param logs The logs entries to persist.
123
+ * @param projectId The ID of the project to persist the log for.
124
+ * @param operationId The ID of the operation to persist the log for.
125
+ * @param stateId The ID of the instance state that produced the log (optional).
126
+ * @param content The log content to append.
117
127
  */
118
- async appendLogs(
128
+ async appendLog(
119
129
  projectId: string,
120
130
  operationId: string,
121
- instanceIds: string[],
122
- logs: string[],
131
+ stateId: string | null,
132
+ content: string,
123
133
  ): Promise<void> {
124
- const records: [string, string][] = logs.map(log => [uuidv7(), log])
125
-
126
- // 1. publish logs to the pubsub system, ignore errors
127
- try {
128
- const events = instanceIds.flatMap(instanceId =>
129
- records.map(
130
- record => [[`operation-instance-log`, operationId, instanceId], record] as const,
131
- ),
132
- )
133
-
134
- await this.pubsubManager.publishMany(events)
135
- } catch (error) {
136
- this.logger.error(
137
- { projectId, operationId, instanceIds, error },
138
- `failed to publish logs for operation "%s" in project "%s"`,
139
- operationId,
140
- projectId,
141
- )
142
- }
134
+ const database = await this.database.forProject(projectId)
143
135
 
144
- // 2. persist logs to the hot state and state repository, ignore errors
145
- try {
146
- // insert logs into the hotstate
147
- await this.hotStateManager.hmset(["operation-logs", projectId], records)
148
- } catch (error) {
149
- this.logger.error(
150
- { projectId, operationId, instanceIds, error },
151
- `failed to append logs for operation "%s" in project "%s"`,
152
- operationId,
153
- projectId,
154
- )
136
+ // verify operation exists
137
+ const operation = await database.operation.findUnique({
138
+ where: { id: operationId },
139
+ select: { id: true, finishedAt: true },
140
+ })
141
+
142
+ if (!operation) {
143
+ throw new OperationNotFoundError(projectId, operationId)
155
144
  }
156
145
 
157
- // 3. persist logs to the state repository, fail on error
158
- try {
159
- await using batch = this.stateManager.batch()
160
-
161
- // insert logs into the operation log repository
162
- await this.stateManager
163
- .getOperationLogRepository(projectId, operationId)
164
- .putMany(records, batch)
165
-
166
- for (const instanceId of instanceIds) {
167
- // create an index for the instance logs
168
- await this.stateManager
169
- .getInstanceLogIndexRepository(projectId, operationId, instanceId)
170
- .indexRepository.putMany(
171
- records.map(([id]) => [id, SAME_KEY]),
172
- batch,
173
- )
174
- }
175
-
176
- await batch.write()
177
- } catch (error) {
178
- this.logger.error(
179
- { projectId, operationId, instanceIds, error },
180
- `failed to persist instance logs for operation "%s" in project "%s"`,
146
+ // store logs in database
147
+ const entry = await database.operationLog.create({
148
+ data: {
149
+ id: ulid(),
181
150
  operationId,
182
- projectId,
183
- )
184
-
185
- throw new Error(
186
- `Failed to persist logs for operation "${operationId}" in project "${projectId}"`,
187
- { cause: error },
188
- )
151
+ stateId,
152
+ content,
153
+ },
154
+ })
155
+
156
+ // publish logs via pubsub - only for logs with stateId (not system logs)
157
+ if (stateId) {
158
+ await this.pubsubManager.publish(["operation-instance-log", operationId, stateId], entry)
189
159
  }
190
160
  }
191
161
 
192
162
  /**
193
- * Retrieves logs for a specific operation and instance.
194
- * First tries to get logs from the hot state, then falls back to the state repository.
163
+ * Retrieves logs for a specific operation and optionally an instance.
195
164
  *
196
165
  * @param projectId The ID of the project to retrieve logs for.
197
166
  * @param operationId The ID of the operation to retrieve logs for.
198
- * @param instanceId The ID of the instance to retrieve logs for.
199
- * @returns An array of log entries as tuples of [logId, logContent].
167
+ * @param stateId Optional instance state ID to filter logs.
168
+ * @returns Array of log entries.
200
169
  */
201
- async getInstanceLogs(
170
+ async getOperationLogs(
202
171
  projectId: string,
203
172
  operationId: string,
204
- instanceId: string,
205
- ): Promise<[string, string][]> {
206
- // 1. first try to get logs from the hot state
207
- try {
208
- const logs = await this.hotStateManager.hgetall(["operation-logs", projectId])
209
-
210
- if (logs.length > 0) {
211
- // if there are logs, assume they are up-to-date
212
- return logs
213
- }
214
-
215
- // otherwise, check if the operation is active to determine if logs are expected
216
- const hasActiveOperation = await this.hotStateManager.hexists(
217
- ["active-operations", projectId],
218
- operationId,
219
- )
220
-
221
- if (hasActiveOperation) {
222
- // if the operation is active, there is just no logs yet
223
- return []
224
- }
225
- } catch (error) {
226
- this.logger.error(
227
- { projectId, operationId, instanceId, error },
228
- `failed to get logs for operation "%s" in project "%s"`,
229
- operationId,
230
- projectId,
231
- )
232
- }
173
+ stateId?: string,
174
+ ): Promise<Array<{ id: string; stateId: string | null; content: string }>> {
175
+ const database = await this.database.forProject(projectId)
233
176
 
234
- // 2. if not found or failed, try to get logs from the state repository
235
- try {
236
- // if no logs are found in the hot state, load them from the state repository
237
- return await this.stateManager
238
- .getInstanceLogIndexRepository(projectId, operationId, instanceId)
239
- .getAll()
240
- } catch (error) {
241
- this.logger.error(
242
- { projectId, operationId, instanceId, error },
243
- `failed to get logs for operation "%s" in project "%s"`,
177
+ const logs = await database.operationLog.findMany({
178
+ where: {
244
179
  operationId,
245
- projectId,
246
- )
180
+ ...(stateId ? { stateId } : {}),
181
+ },
182
+ orderBy: { id: "asc" },
183
+ select: {
184
+ id: true,
185
+ stateId: true,
186
+ content: true,
187
+ },
188
+ })
189
+
190
+ return logs
191
+ }
247
192
 
248
- return []
249
- }
193
+ /**
194
+ * Marks an operation as finished with given status.
195
+ *
196
+ * @param projectId The project ID containing the operation.
197
+ * @param operationId The operation ID to complete.
198
+ * @returns The updated operation.
199
+ */
200
+ async markOperationFinished(
201
+ projectId: string,
202
+ operationId: string,
203
+ status: OperationStatus,
204
+ ): Promise<Operation> {
205
+ const result = await this.updateOperation(projectId, operationId, {
206
+ status,
207
+ finishedAt: new Date(),
208
+ })
209
+
210
+ this.logger.info(
211
+ { projectId, operationId, status },
212
+ `marked operation as finished with status "${status}"`,
213
+ )
214
+
215
+ return result
250
216
  }
251
217
  }
@@ -0,0 +1,258 @@
1
+ import type { InstanceModel } from "@highstate/contract"
2
+ import type { Logger } from "pino"
3
+ import type { DatabaseManager } from "../database"
4
+ import type { LibraryBackend } from "../library"
5
+ import type { ProjectModelBackend } from "../project-model"
6
+ import type { InstanceStateService } from "./instance-state"
7
+ import type { ProjectUnlockService } from "./project-unlock"
8
+ import { isNonNullish } from "remeda"
9
+ import {
10
+ type FullProjectModel,
11
+ forSchema,
12
+ InputResolver,
13
+ type InputResolverNode,
14
+ ProjectNotFoundError,
15
+ type ProjectOutput,
16
+ projectOutputSchema,
17
+ type ResolvedInstanceInput,
18
+ } from "../shared"
19
+
20
+ export type GetProjectModelOptions = {
21
+ /**
22
+ * Whether to include virtual instances in the model.
23
+ *
24
+ * By default, virtual instances are not included.
25
+ */
26
+ includeVirtualInstances?: boolean
27
+
28
+ /**
29
+ * Whether to include ghost instances in the model.
30
+ *
31
+ * By default, ghost instances are not included.
32
+ */
33
+ includeGhostInstances?: boolean
34
+ }
35
+
36
+ export class ProjectModelService {
37
+ constructor(
38
+ private readonly database: DatabaseManager,
39
+ private readonly libraryBackend: LibraryBackend,
40
+ private readonly instanceStateService: InstanceStateService,
41
+ private readonly projectModelBackends: Record<string, ProjectModelBackend>,
42
+ private readonly projectUnlockService: ProjectUnlockService,
43
+ private readonly logger: Logger,
44
+ ) {
45
+ this.projectUnlockService.registerUnlockTask(
46
+ //
47
+ "sync-instance-states",
48
+ projectId => this.syncInstanceStates(projectId),
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Get the project model containing instances, hubs + virtual instances and ghost instances if requested.
54
+ *
55
+ * @param projectId The ID of the project to get the model for.
56
+ * @param options Options to control the model retrieval.
57
+ */
58
+ async getProjectModel(
59
+ projectId: string,
60
+ { includeVirtualInstances = false, includeGhostInstances = false }: GetProjectModelOptions = {},
61
+ ): Promise<[projectModel: FullProjectModel, project: ProjectOutput]> {
62
+ const { project, backend, spec } = await this.getProjectWithBackend(projectId)
63
+
64
+ // get base model from storage backend
65
+ const { instances, hubs } = await backend.getProjectModel(project, spec)
66
+
67
+ return [
68
+ {
69
+ instances,
70
+ hubs,
71
+ virtualInstances: includeVirtualInstances ? await this.getVirtualInstances(projectId) : [],
72
+ ghostInstances: includeGhostInstances
73
+ ? await this.getGhostInstances(
74
+ projectId,
75
+ instances.map(instance => instance.id),
76
+ )
77
+ : [],
78
+ },
79
+ project,
80
+ ]
81
+ }
82
+
83
+ /**
84
+ * Resolve a project by loading all dependencies and processing input resolution.
85
+ * Does not load virtual instances.
86
+ *
87
+ * @param projectId The ID of the project to resolve.
88
+ */
89
+ async resolveProject(projectId: string) {
90
+ const [[{ instances, hubs }, project], states] = await Promise.all([
91
+ this.getProjectModel(projectId),
92
+ this.instanceStateService.getInstanceStates(projectId, { includeEvaluationState: true }),
93
+ ])
94
+
95
+ const library = await this.libraryBackend.loadLibrary(project.libraryId)
96
+
97
+ const filteredInstances = instances.filter(instance => instance.type in library.components)
98
+ const stateMap = new Map(states.map(state => [state.id, state]))
99
+
100
+ const inputResolverNodes = new Map<string, InputResolverNode>()
101
+
102
+ for (const instance of filteredInstances) {
103
+ inputResolverNodes.set(`instance:${instance.id}`, {
104
+ kind: "instance",
105
+ instance,
106
+ component: library.components[instance.type],
107
+ })
108
+ }
109
+
110
+ for (const hub of hubs) {
111
+ inputResolverNodes.set(`hub:${hub.id}`, { kind: "hub", hub })
112
+ }
113
+
114
+ const inputResolver = new InputResolver(inputResolverNodes, this.logger)
115
+ inputResolver.addAllNodesToWorkset()
116
+
117
+ const resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>> = {}
118
+
119
+ await inputResolver.process()
120
+
121
+ for (const instance of filteredInstances) {
122
+ const output = inputResolver.requireOutput(`instance:${instance.id}`)
123
+ if (output.kind !== "instance") {
124
+ throw new Error("Expected instance node")
125
+ }
126
+
127
+ resolvedInputs[instance.id] = output.resolvedInputs
128
+ }
129
+
130
+ return {
131
+ project,
132
+ library,
133
+ instances: filteredInstances,
134
+ stateMap,
135
+ resolvedInputs,
136
+ }
137
+ }
138
+
139
+ /**
140
+ /**
141
+ * Get the appropriate project model backend for the given project.
142
+ *
143
+ * @param type The project storage type.
144
+ * @returns The project model backend.
145
+ */
146
+ private getProjectModelBackend(type: string): ProjectModelBackend {
147
+ const backend = this.projectModelBackends[type]
148
+ if (!backend) {
149
+ throw new Error(`Project model backend not found for type: ${type}`)
150
+ }
151
+
152
+ return backend
153
+ }
154
+
155
+ /**
156
+ * Get project with model backend and spec.
157
+ *
158
+ * @param projectId The project ID to get.
159
+ * @returns The project, backend, and spec.
160
+ */
161
+ private async getProjectWithBackend(projectId: string) {
162
+ const project = await this.database.backend.project.findUnique({
163
+ where: { id: projectId },
164
+ select: {
165
+ ...forSchema(projectOutputSchema),
166
+ modelStorage: {
167
+ select: {
168
+ spec: true,
169
+ },
170
+ },
171
+ },
172
+ })
173
+
174
+ if (!project) {
175
+ throw new ProjectNotFoundError(projectId)
176
+ }
177
+
178
+ const backend = this.getProjectModelBackend(project.modelStorage.spec.type)
179
+
180
+ return {
181
+ project,
182
+ backend,
183
+ spec: project.modelStorage.spec,
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Get virtual instances from the database for the given project ID.
189
+ *
190
+ * @param projectId The project ID to get virtual instances for.
191
+ * @returns The list of virtual instances.
192
+ */
193
+ private async getVirtualInstances(projectId: string): Promise<InstanceModel[]> {
194
+ const database = await this.database.forProject(projectId)
195
+ const states = await database.instanceEvaluationState.findMany({ select: { model: true } })
196
+
197
+ return states.map(state => state.model).filter(isNonNullish)
198
+ }
199
+
200
+ /**
201
+ * Get ghost instances from the database for the given project ID.
202
+ *
203
+ * @param projectId The project ID to get ghost instances for.
204
+ * @param residentInstanceIds The IDs of the instances present in the model.
205
+ * @return The list of ghost instances.
206
+ */
207
+ private async getGhostInstances(
208
+ projectId: string,
209
+ residentInstanceIds: string[],
210
+ ): Promise<InstanceModel[]> {
211
+ const database = await this.database.forProject(projectId)
212
+
213
+ const states = await database.instanceState.findMany({
214
+ where: {
215
+ // undeployed instances cannot be considered ghost
216
+ status: { not: "undeployed" },
217
+
218
+ OR: [
219
+ // the resident instance is ghost if it is not in the model
220
+ { source: "resident", instanceId: { notIn: residentInstanceIds } },
221
+
222
+ // the virtual instance is ghost if it is has no evaluation state
223
+ { source: "virtual", evaluationState: null },
224
+ ],
225
+ },
226
+ select: { model: true },
227
+ })
228
+
229
+ return states.map(state => state.model).filter(isNonNullish)
230
+ }
231
+
232
+ private async syncInstanceStates(projectId: string): Promise<void> {
233
+ const database = await this.database.forProject(projectId)
234
+
235
+ await database.$transaction(async tx => {
236
+ const [{ instances }] = await this.getProjectModel(projectId)
237
+
238
+ const existingStates = await tx.instanceState.findMany({ select: { instanceId: true } })
239
+ const existingStateIds = new Set(existingStates.map(state => state.instanceId))
240
+ const missingInstances = instances.filter(instance => !existingStateIds.has(instance.id))
241
+
242
+ if (missingInstances.length === 0) {
243
+ return
244
+ }
245
+
246
+ await tx.instanceState.createMany({
247
+ data: missingInstances.map(instance => ({
248
+ instanceId: instance.id,
249
+ kind: instance.kind,
250
+ source: "resident",
251
+ status: "undeployed",
252
+ })),
253
+ })
254
+
255
+ this.logger.info({ projectId }, "created missing %s instance states", missingInstances.length)
256
+ })
257
+ }
258
+ }