@quadnix/octo 0.0.30 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +117 -8
  3. package/dist/app.type.d.ts +277 -19
  4. package/dist/app.type.js +80 -9
  5. package/dist/app.type.js.map +1 -1
  6. package/dist/decorators/action.decorator.d.ts +10 -6
  7. package/dist/decorators/action.decorator.js +19 -6
  8. package/dist/decorators/action.decorator.js.map +1 -1
  9. package/dist/decorators/anchor.decorator.d.ts +12 -5
  10. package/dist/decorators/anchor.decorator.js +12 -5
  11. package/dist/decorators/anchor.decorator.js.map +1 -1
  12. package/dist/decorators/enable-hook.decorator.d.ts +1 -1
  13. package/dist/decorators/enable-hook.decorator.js +1 -1
  14. package/dist/decorators/factory.decorator.d.ts +18 -34
  15. package/dist/decorators/factory.decorator.js +18 -34
  16. package/dist/decorators/factory.decorator.js.map +1 -1
  17. package/dist/decorators/model.decorator.d.ts +4 -6
  18. package/dist/decorators/model.decorator.js +2 -7
  19. package/dist/decorators/model.decorator.js.map +1 -1
  20. package/dist/decorators/module.decorator.d.ts +16 -10
  21. package/dist/decorators/module.decorator.js +16 -10
  22. package/dist/decorators/module.decorator.js.map +1 -1
  23. package/dist/decorators/on-event.decorator.d.ts +7 -2
  24. package/dist/decorators/on-event.decorator.js +5 -0
  25. package/dist/decorators/on-event.decorator.js.map +1 -1
  26. package/dist/decorators/resource.decorator.d.ts +50 -1
  27. package/dist/decorators/resource.decorator.js +38 -1
  28. package/dist/decorators/resource.decorator.js.map +1 -1
  29. package/dist/decorators/validate.decorator.d.ts +15 -1
  30. package/dist/decorators/validate.decorator.js +15 -1
  31. package/dist/decorators/validate.decorator.js.map +1 -1
  32. package/dist/errors/container.error.d.ts +18 -0
  33. package/dist/errors/container.error.js +18 -0
  34. package/dist/errors/container.error.js.map +1 -1
  35. package/dist/errors/index.d.ts +1 -0
  36. package/dist/errors/index.js +1 -0
  37. package/dist/errors/index.js.map +1 -1
  38. package/dist/errors/module.error.d.ts +6 -0
  39. package/dist/errors/module.error.js +6 -0
  40. package/dist/errors/module.error.js.map +1 -1
  41. package/dist/errors/node.error.d.ts +56 -0
  42. package/dist/errors/node.error.js +56 -0
  43. package/dist/errors/node.error.js.map +1 -1
  44. package/dist/errors/schema.error.d.ts +14 -1
  45. package/dist/errors/schema.error.js +11 -1
  46. package/dist/errors/schema.error.js.map +1 -1
  47. package/dist/errors/terraform.error.d.ts +20 -0
  48. package/dist/errors/terraform.error.js +24 -0
  49. package/dist/errors/terraform.error.js.map +1 -0
  50. package/dist/errors/transaction.error.d.ts +93 -2
  51. package/dist/errors/transaction.error.js +83 -0
  52. package/dist/errors/transaction.error.js.map +1 -1
  53. package/dist/events/action.event.d.ts +8 -4
  54. package/dist/events/action.event.js +2 -4
  55. package/dist/events/action.event.js.map +1 -1
  56. package/dist/events/command.event.d.ts +43 -0
  57. package/dist/events/command.event.js +33 -0
  58. package/dist/events/command.event.js.map +1 -0
  59. package/dist/events/event.model.d.ts +26 -8
  60. package/dist/events/event.model.js +15 -7
  61. package/dist/events/event.model.js.map +1 -1
  62. package/dist/events/hook/commit.hook.event.d.ts +1 -7
  63. package/dist/events/hook/commit.hook.event.js +0 -6
  64. package/dist/events/hook/commit.hook.event.js.map +1 -1
  65. package/dist/events/hook/hook.event.d.ts +1 -3
  66. package/dist/events/hook/hook.event.js +0 -2
  67. package/dist/events/hook/hook.event.js.map +1 -1
  68. package/dist/events/hook/model.hook.event.d.ts +1 -7
  69. package/dist/events/hook/model.hook.event.js +0 -6
  70. package/dist/events/hook/model.hook.event.js.map +1 -1
  71. package/dist/events/hook/resource.hook.event.d.ts +1 -7
  72. package/dist/events/hook/resource.hook.event.js +0 -6
  73. package/dist/events/hook/resource.hook.event.js.map +1 -1
  74. package/dist/events/index.d.ts +5 -0
  75. package/dist/events/index.js +5 -0
  76. package/dist/events/index.js.map +1 -1
  77. package/dist/events/mode-result.event.d.ts +57 -0
  78. package/dist/events/mode-result.event.js +24 -0
  79. package/dist/events/mode-result.event.js.map +1 -0
  80. package/dist/events/mode.event.d.ts +61 -0
  81. package/dist/events/mode.event.js +32 -0
  82. package/dist/events/mode.event.js.map +1 -0
  83. package/dist/events/module.event.d.ts +7 -5
  84. package/dist/events/module.event.js +3 -4
  85. package/dist/events/module.event.js.map +1 -1
  86. package/dist/events/registration.event.d.ts +19 -15
  87. package/dist/events/registration.event.js +18 -14
  88. package/dist/events/registration.event.js.map +1 -1
  89. package/dist/events/serialization.event.d.ts +16 -13
  90. package/dist/events/serialization.event.js +15 -12
  91. package/dist/events/serialization.event.js.map +1 -1
  92. package/dist/events/state.event.d.ts +102 -0
  93. package/dist/events/state.event.js +69 -0
  94. package/dist/events/state.event.js.map +1 -0
  95. package/dist/events/terraform.event.d.ts +78 -0
  96. package/dist/events/terraform.event.js +55 -0
  97. package/dist/events/terraform.event.js.map +1 -0
  98. package/dist/events/transaction.event.d.ts +3 -40
  99. package/dist/events/transaction.event.js +1 -29
  100. package/dist/events/transaction.event.js.map +1 -1
  101. package/dist/functions/container/container.d.ts +54 -13
  102. package/dist/functions/container/container.js +64 -19
  103. package/dist/functions/container/container.js.map +1 -1
  104. package/dist/functions/container/test-container.d.ts +43 -29
  105. package/dist/functions/container/test-container.js +71 -43
  106. package/dist/functions/container/test-container.js.map +1 -1
  107. package/dist/functions/dependency/dependency.d.ts +84 -23
  108. package/dist/functions/dependency/dependency.js +84 -14
  109. package/dist/functions/dependency/dependency.js.map +1 -1
  110. package/dist/functions/diff/aggregate-resource-actions.d.ts +19 -0
  111. package/dist/functions/diff/aggregate-resource-actions.js +41 -0
  112. package/dist/functions/diff/aggregate-resource-actions.js.map +1 -0
  113. package/dist/functions/diff/diff-metadata.d.ts +21 -3
  114. package/dist/functions/diff/diff-metadata.js +18 -0
  115. package/dist/functions/diff/diff-metadata.js.map +1 -1
  116. package/dist/functions/diff/diff.d.ts +68 -7
  117. package/dist/functions/diff/diff.js +44 -3
  118. package/dist/functions/diff/diff.js.map +1 -1
  119. package/dist/functions/hook/commit.hook.d.ts +16 -1
  120. package/dist/functions/hook/commit.hook.js +21 -6
  121. package/dist/functions/hook/commit.hook.js.map +1 -1
  122. package/dist/functions/hook/model-action.hook.d.ts +16 -1
  123. package/dist/functions/hook/model-action.hook.js +21 -6
  124. package/dist/functions/hook/model-action.hook.js.map +1 -1
  125. package/dist/functions/hook/resource-action.hook.d.ts +16 -1
  126. package/dist/functions/hook/resource-action.hook.js +21 -6
  127. package/dist/functions/hook/resource-action.hook.js.map +1 -1
  128. package/dist/functions/node/node-action.abstract.d.ts +23 -2
  129. package/dist/functions/node/node-action.abstract.js +22 -8
  130. package/dist/functions/node/node-action.abstract.js.map +1 -1
  131. package/dist/functions/node/node.abstract.d.ts +56 -5
  132. package/dist/functions/node/node.abstract.js +106 -22
  133. package/dist/functions/node/node.abstract.js.map +1 -1
  134. package/dist/functions/node/node.interface.d.ts +3 -1
  135. package/dist/functions/schema/schema.d.ts +7 -0
  136. package/dist/functions/schema/schema.js +10 -1
  137. package/dist/functions/schema/schema.js.map +1 -1
  138. package/dist/index.d.ts +12 -6
  139. package/dist/index.js +9 -4
  140. package/dist/index.js.map +1 -1
  141. package/dist/index.type.d.ts +5 -0
  142. package/dist/index.type.js +2 -0
  143. package/dist/index.type.js.map +1 -0
  144. package/dist/main.d.ts +245 -14
  145. package/dist/main.js +377 -72
  146. package/dist/main.js.map +1 -1
  147. package/dist/models/account/account.model.d.ts +43 -7
  148. package/dist/models/account/account.model.js +56 -8
  149. package/dist/models/account/account.model.js.map +1 -1
  150. package/dist/models/account/account.schema.d.ts +13 -1
  151. package/dist/models/account/account.schema.js +12 -0
  152. package/dist/models/account/account.schema.js.map +1 -1
  153. package/dist/models/app/app.model.d.ts +13 -8
  154. package/dist/models/app/app.model.js +17 -32
  155. package/dist/models/app/app.model.js.map +1 -1
  156. package/dist/models/app/app.schema.d.ts +6 -0
  157. package/dist/models/app/app.schema.js +6 -0
  158. package/dist/models/app/app.schema.js.map +1 -1
  159. package/dist/models/deployment/deployment.model.d.ts +13 -1
  160. package/dist/models/deployment/deployment.model.js +16 -4
  161. package/dist/models/deployment/deployment.model.js.map +1 -1
  162. package/dist/models/deployment/deployment.schema.d.ts +6 -0
  163. package/dist/models/deployment/deployment.schema.js +6 -0
  164. package/dist/models/deployment/deployment.schema.js.map +1 -1
  165. package/dist/models/environment/environment.model.d.ts +15 -3
  166. package/dist/models/environment/environment.model.js +22 -10
  167. package/dist/models/environment/environment.model.js.map +1 -1
  168. package/dist/models/environment/environment.schema.d.ts +2 -0
  169. package/dist/models/environment/environment.schema.js +2 -0
  170. package/dist/models/environment/environment.schema.js.map +1 -1
  171. package/dist/models/execution/execution.model.d.ts +21 -3
  172. package/dist/models/execution/execution.model.js +47 -29
  173. package/dist/models/execution/execution.model.js.map +1 -1
  174. package/dist/models/execution/execution.schema.d.ts +11 -0
  175. package/dist/models/execution/execution.schema.js +11 -0
  176. package/dist/models/execution/execution.schema.js.map +1 -1
  177. package/dist/models/filesystem/filesystem.model.d.ts +27 -1
  178. package/dist/models/filesystem/filesystem.model.js +30 -4
  179. package/dist/models/filesystem/filesystem.model.js.map +1 -1
  180. package/dist/models/filesystem/filesystem.schema.d.ts +5 -0
  181. package/dist/models/filesystem/filesystem.schema.js +5 -0
  182. package/dist/models/filesystem/filesystem.schema.js.map +1 -1
  183. package/dist/models/image/image.model.d.ts +19 -5
  184. package/dist/models/image/image.model.js +25 -11
  185. package/dist/models/image/image.model.js.map +1 -1
  186. package/dist/models/image/image.schema.d.ts +8 -0
  187. package/dist/models/image/image.schema.js +8 -0
  188. package/dist/models/image/image.schema.js.map +1 -1
  189. package/dist/models/model-action.interface.d.ts +17 -4
  190. package/dist/models/model.abstract.d.ts +96 -1
  191. package/dist/models/model.abstract.js +107 -8
  192. package/dist/models/model.abstract.js.map +1 -1
  193. package/dist/models/model.interface.d.ts +3 -0
  194. package/dist/models/pipeline/pipeline.model.d.ts +14 -2
  195. package/dist/models/pipeline/pipeline.model.js +19 -7
  196. package/dist/models/pipeline/pipeline.model.js.map +1 -1
  197. package/dist/models/pipeline/pipeline.schema.d.ts +8 -0
  198. package/dist/models/pipeline/pipeline.schema.js +8 -0
  199. package/dist/models/pipeline/pipeline.schema.js.map +1 -1
  200. package/dist/models/region/region.model.d.ts +16 -1
  201. package/dist/models/region/region.model.js +19 -4
  202. package/dist/models/region/region.model.js.map +1 -1
  203. package/dist/models/region/region.schema.d.ts +6 -0
  204. package/dist/models/region/region.schema.js +6 -0
  205. package/dist/models/region/region.schema.js.map +1 -1
  206. package/dist/models/server/server.model.d.ts +15 -3
  207. package/dist/models/server/server.model.js +18 -6
  208. package/dist/models/server/server.model.js.map +1 -1
  209. package/dist/models/server/server.schema.d.ts +3 -1
  210. package/dist/models/server/server.schema.js +3 -1
  211. package/dist/models/server/server.schema.js.map +1 -1
  212. package/dist/models/service/service.model.d.ts +37 -6
  213. package/dist/models/service/service.model.js +65 -11
  214. package/dist/models/service/service.model.js.map +1 -1
  215. package/dist/models/service/service.schema.d.ts +48 -1
  216. package/dist/models/service/service.schema.js +56 -1
  217. package/dist/models/service/service.schema.js.map +1 -1
  218. package/dist/models/subnet/subnet.model.d.ts +27 -3
  219. package/dist/models/subnet/subnet.model.js +40 -16
  220. package/dist/models/subnet/subnet.model.js.map +1 -1
  221. package/dist/models/subnet/subnet.schema.d.ts +9 -0
  222. package/dist/models/subnet/subnet.schema.js +9 -0
  223. package/dist/models/subnet/subnet.schema.js.map +1 -1
  224. package/dist/modes/apply-transaction.d.ts +23 -0
  225. package/dist/modes/apply-transaction.js +25 -0
  226. package/dist/modes/apply-transaction.js.map +1 -0
  227. package/dist/modes/commit.mode.d.ts +54 -0
  228. package/dist/modes/commit.mode.js +235 -0
  229. package/dist/modes/commit.mode.js.map +1 -0
  230. package/dist/modes/generate.mode.d.ts +30 -0
  231. package/dist/modes/generate.mode.js +50 -0
  232. package/dist/modes/generate.mode.js.map +1 -0
  233. package/dist/modes/run-action.mode.d.ts +30 -0
  234. package/dist/modes/run-action.mode.js +140 -0
  235. package/dist/modes/run-action.mode.js.map +1 -0
  236. package/dist/modes/validate.mode.d.ts +186 -0
  237. package/dist/modes/validate.mode.js +558 -0
  238. package/dist/modes/validate.mode.js.map +1 -0
  239. package/dist/modules/module.abstract.d.ts +51 -0
  240. package/dist/modules/module.abstract.js +36 -0
  241. package/dist/modules/module.abstract.js.map +1 -1
  242. package/dist/modules/module.container.d.ts +43 -1
  243. package/dist/modules/module.container.js +77 -5
  244. package/dist/modules/module.container.js.map +1 -1
  245. package/dist/modules/test-module.container.d.ts +139 -25
  246. package/dist/modules/test-module.container.js +383 -199
  247. package/dist/modules/test-module.container.js.map +1 -1
  248. package/dist/overlays/anchor.abstract.d.ts +32 -1
  249. package/dist/overlays/anchor.abstract.js +42 -11
  250. package/dist/overlays/anchor.abstract.js.map +1 -1
  251. package/dist/overlays/anchor.schema.d.ts +21 -0
  252. package/dist/overlays/anchor.schema.js +21 -0
  253. package/dist/overlays/anchor.schema.js.map +1 -1
  254. package/dist/overlays/overlay-data.repository.d.ts +18 -0
  255. package/dist/overlays/overlay-data.repository.js +18 -0
  256. package/dist/overlays/overlay-data.repository.js.map +1 -1
  257. package/dist/overlays/overlay.abstract.d.ts +56 -1
  258. package/dist/overlays/overlay.abstract.js +79 -20
  259. package/dist/overlays/overlay.abstract.js.map +1 -1
  260. package/dist/overlays/overlay.schema.d.ts +20 -0
  261. package/dist/overlays/overlay.schema.js +20 -0
  262. package/dist/overlays/overlay.schema.js.map +1 -1
  263. package/dist/resources/resource-action.interface.d.ts +29 -3
  264. package/dist/resources/resource-data.repository.d.ts +63 -1
  265. package/dist/resources/resource-data.repository.js +94 -4
  266. package/dist/resources/resource-data.repository.js.map +1 -1
  267. package/dist/resources/resource.abstract.d.ts +163 -6
  268. package/dist/resources/resource.abstract.js +223 -59
  269. package/dist/resources/resource.abstract.js.map +1 -1
  270. package/dist/resources/resource.interface.d.ts +0 -2
  271. package/dist/resources/resource.schema.d.ts +39 -1
  272. package/dist/resources/resource.schema.js +38 -0
  273. package/dist/resources/resource.schema.js.map +1 -1
  274. package/dist/resources/terraform-resource.abstract.d.ts +44 -0
  275. package/dist/resources/terraform-resource.abstract.js +45 -0
  276. package/dist/resources/terraform-resource.abstract.js.map +1 -0
  277. package/dist/services/event/event.service.d.ts +32 -5
  278. package/dist/services/event/event.service.js +58 -16
  279. package/dist/services/event/event.service.js.map +1 -1
  280. package/dist/services/input/input.service.d.ts +82 -5
  281. package/dist/services/input/input.service.js +143 -12
  282. package/dist/services/input/input.service.js.map +1 -1
  283. package/dist/services/schema-translation/schema-translation.service.d.ts +9 -0
  284. package/dist/services/schema-translation/schema-translation.service.js +9 -0
  285. package/dist/services/schema-translation/schema-translation.service.js.map +1 -1
  286. package/dist/services/serialization/model/model-serialization.service.d.ts +25 -3
  287. package/dist/services/serialization/model/model-serialization.service.js +113 -62
  288. package/dist/services/serialization/model/model-serialization.service.js.map +1 -1
  289. package/dist/services/serialization/resource/resource-serialization.service.d.ts +36 -6
  290. package/dist/services/serialization/resource/resource-serialization.service.js +77 -28
  291. package/dist/services/serialization/resource/resource-serialization.service.js.map +1 -1
  292. package/dist/services/state-management/local.state-provider.d.ts +67 -4
  293. package/dist/services/state-management/local.state-provider.js +168 -41
  294. package/dist/services/state-management/local.state-provider.js.map +1 -1
  295. package/dist/services/state-management/remote.state-provider.d.ts +216 -0
  296. package/dist/services/state-management/remote.state-provider.js +645 -0
  297. package/dist/services/state-management/remote.state-provider.js.map +1 -0
  298. package/dist/services/state-management/state-management.service.d.ts +195 -17
  299. package/dist/services/state-management/state-management.service.js +259 -10
  300. package/dist/services/state-management/state-management.service.js.map +1 -1
  301. package/dist/services/state-management/state-provider.interface.d.ts +98 -3
  302. package/dist/services/state-management/state-provider.interface.js +14 -1
  303. package/dist/services/state-management/state-provider.interface.js.map +1 -1
  304. package/dist/services/state-management/terraform-state.service.d.ts +99 -0
  305. package/dist/services/state-management/terraform-state.service.js +197 -0
  306. package/dist/services/state-management/terraform-state.service.js.map +1 -0
  307. package/dist/services/state-management/test.state-provider.d.ts +42 -4
  308. package/dist/services/state-management/test.state-provider.js +77 -25
  309. package/dist/services/state-management/test.state-provider.js.map +1 -1
  310. package/dist/services/terraform/terraform.service.d.ts +480 -0
  311. package/dist/services/terraform/terraform.service.js +1082 -0
  312. package/dist/services/terraform/terraform.service.js.map +1 -0
  313. package/dist/services/terraform/terraform.type.d.ts +387 -0
  314. package/dist/services/terraform/terraform.type.js +579 -0
  315. package/dist/services/terraform/terraform.type.js.map +1 -0
  316. package/dist/services/transaction/transaction.service.d.ts +110 -10
  317. package/dist/services/transaction/transaction.service.js +570 -251
  318. package/dist/services/transaction/transaction.service.js.map +1 -1
  319. package/dist/services/validation/validation.service.d.ts +15 -3
  320. package/dist/services/validation/validation.service.js +12 -0
  321. package/dist/services/validation/validation.service.js.map +1 -1
  322. package/dist/utilities/array/array.utility.d.ts +3 -0
  323. package/dist/utilities/array/array.utility.js +3 -0
  324. package/dist/utilities/array/array.utility.js.map +1 -1
  325. package/dist/{functions → utilities}/diff/diff.utility.d.ts +19 -3
  326. package/dist/{functions → utilities}/diff/diff.utility.js +57 -41
  327. package/dist/utilities/diff/diff.utility.js.map +1 -0
  328. package/dist/utilities/file/file.utility.d.ts +16 -2
  329. package/dist/utilities/file/file.utility.js +23 -61
  330. package/dist/utilities/file/file.utility.js.map +1 -1
  331. package/dist/utilities/hcl/hcl.utility.d.ts +128 -0
  332. package/dist/utilities/hcl/hcl.utility.js +402 -0
  333. package/dist/utilities/hcl/hcl.utility.js.map +1 -0
  334. package/dist/utilities/http/http.utility.d.ts +57 -0
  335. package/dist/utilities/http/http.utility.js +98 -0
  336. package/dist/utilities/http/http.utility.js.map +1 -0
  337. package/dist/utilities/node/node.utility.d.ts +3 -0
  338. package/dist/utilities/node/node.utility.js +3 -0
  339. package/dist/utilities/node/node.utility.js.map +1 -1
  340. package/dist/utilities/object/object.utility.d.ts +12 -0
  341. package/dist/utilities/object/object.utility.js +12 -0
  342. package/dist/utilities/object/object.utility.js.map +1 -1
  343. package/dist/utilities/project/project.utility.d.ts +28 -0
  344. package/dist/utilities/project/project.utility.js +35 -0
  345. package/dist/utilities/project/project.utility.js.map +1 -0
  346. package/dist/utilities/string/string.utility.d.ts +41 -0
  347. package/dist/utilities/string/string.utility.js +65 -0
  348. package/dist/utilities/string/string.utility.js.map +1 -0
  349. package/dist/utilities/terraform/terraform.utility.d.ts +276 -0
  350. package/dist/utilities/terraform/terraform.utility.js +623 -0
  351. package/dist/utilities/terraform/terraform.utility.js.map +1 -0
  352. package/dist/utilities/test-helpers/test-classes.d.ts +36 -30
  353. package/dist/utilities/test-helpers/test-classes.js +38 -39
  354. package/dist/utilities/test-helpers/test-classes.js.map +1 -1
  355. package/dist/utilities/test-helpers/test-models.d.ts +8 -1
  356. package/dist/utilities/test-helpers/test-models.js +37 -12
  357. package/dist/utilities/test-helpers/test-models.js.map +1 -1
  358. package/dist/utilities/test-helpers/test-modes.d.ts +315 -0
  359. package/dist/utilities/test-helpers/test-modes.js +486 -0
  360. package/dist/utilities/test-helpers/test-modes.js.map +1 -0
  361. package/dist/utilities/test-helpers/test-modules.d.ts +47 -0
  362. package/dist/utilities/test-helpers/test-modules.js +85 -0
  363. package/dist/utilities/test-helpers/test-modules.js.map +1 -0
  364. package/dist/utilities/test-helpers/test-overlays.d.ts +20 -2
  365. package/dist/utilities/test-helpers/test-overlays.js +56 -6
  366. package/dist/utilities/test-helpers/test-overlays.js.map +1 -1
  367. package/dist/utilities/test-helpers/test-resources.d.ts +23 -3
  368. package/dist/utilities/test-helpers/test-resources.js +83 -23
  369. package/dist/utilities/test-helpers/test-resources.js.map +1 -1
  370. package/dist/utilities/validation/validation.utility.d.ts +24 -0
  371. package/dist/utilities/validation/validation.utility.js +24 -0
  372. package/dist/utilities/validation/validation.utility.js.map +1 -1
  373. package/package.json +12 -6
  374. package/dist/functions/diff/diff.utility.js.map +0 -1
  375. package/dist/services/capture/capture.service.d.ts +0 -19
  376. package/dist/services/capture/capture.service.js +0 -43
  377. package/dist/services/capture/capture.service.js.map +0 -1
  378. package/dist/services/state-management/local-encryption.state-provider.d.ts +0 -20
  379. package/dist/services/state-management/local-encryption.state-provider.js +0 -90
  380. package/dist/services/state-management/local-encryption.state-provider.js.map +0 -1
  381. package/dist/utilities/cidr/cidr.utility.d.ts +0 -10
  382. package/dist/utilities/cidr/cidr.utility.js +0 -51
  383. package/dist/utilities/cidr/cidr.utility.js.map +0 -1
@@ -0,0 +1,645 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { copyFile, link, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { hostname } from 'node:os';
4
+ import { basename, dirname, extname, join, resolve } from 'node:path';
5
+ import { AppLockError, TransactionError } from '../../errors/index.js';
6
+ import { FileUtility } from '../../utilities/file/file.utility.js';
7
+ import { HttpUtility } from '../../utilities/http/http.utility.js';
8
+ import { LocalStateProvider } from './local.state-provider.js';
9
+ import { JOURNAL_STATE_FILE_NAME_PREFIX } from './state-provider.interface.js';
10
+ const APP_LOCK_SCOPE = ['app'];
11
+ const LOCK_FILE_NAME = 'remote-state.lock';
12
+ const STATE_FILE_EXTENSIONS = ['.json', '.tfstate'];
13
+ const STATE_FILE_NAME_MAX_LENGTH = 255;
14
+ const STATE_FILE_NAME_PATTERN = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
15
+ const UPLOAD_LOCK_FILE_NAME = 'remote-state-upload.lock';
16
+ const UPLOAD_LOCK_RETRY_IN_MS = 50;
17
+ /**
18
+ * Keeps state in Octo's managed backend, so a team shares one store and no machine holds the only copy.
19
+ *
20
+ * A run reads and writes a state directory on disk, exactly as {@link LocalStateProvider} does, and
21
+ * the backend is kept in step with it:
22
+ *
23
+ * - {@link RemoteStateProvider.refreshState} brings the directory up to the newest sealed state, and
24
+ * is what `octo generate` plans against.
25
+ * - {@link RemoteStateProvider.lockApp} opens a new record for the run, refusing when the newest
26
+ * sealed state has moved since the directory was refreshed.
27
+ * - Every write under the lock lands in the directory and in the run's record before it returns, so
28
+ * a run that dies leaves a record somebody can finish from another machine.
29
+ * - {@link RemoteStateProvider.unlockApp} seals the record and releases the lock.
30
+ *
31
+ * Two files of the provider's own sit beside the state files and are never uploaded:
32
+ * `remote-state.lock` records the run the directory belongs to and is deleted when the lock is
33
+ * released, and `remote-state-upload.lock` makes the octo processes of one machine write to the
34
+ * backend one at a time and is deleted after every write.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * await octo.initialize(
39
+ * new RemoteStateProvider({
40
+ * appId: 'my-app',
41
+ * localStateDirectoryPath: './.octo',
42
+ * organizationId: 'my-organization',
43
+ * sessionId: process.env.OCTO_SESSION!,
44
+ * stateManagementServiceUrl: 'http://localhost:3619/state',
45
+ * }),
46
+ * );
47
+ * ```
48
+ *
49
+ * @group Services/State Management
50
+ *
51
+ * @see Techniques for [State Providers](/docs/techniques/state-providers).
52
+ */
53
+ export class RemoteStateProvider {
54
+ /**
55
+ * @param options `organizationId` and `appId` name the app whose state this is, and `sessionId`
56
+ * is the session the run acts as. `stateManagementServiceUrl` is the backend service, including
57
+ * the prefix its routes sit behind. `localStateDirectoryPath` is the directory state files are
58
+ * written into. `lockContext` is what a blocked run is shown about the holder.
59
+ */
60
+ constructor(options) {
61
+ this.appId = options.appId;
62
+ this.localStateDirectoryPath = resolve(options.localStateDirectoryPath);
63
+ this.localStateProvider = new LocalStateProvider(options.localStateDirectoryPath);
64
+ this.lockContext = options.lockContext ?? `octo on ${hostname()}`;
65
+ this.organizationId = options.organizationId;
66
+ this.sessionId = options.sessionId;
67
+ this.stateManagementServiceUrl = options.stateManagementServiceUrl.replace(/\/+$/, '');
68
+ }
69
+ /**
70
+ * The id of the lock currently held, or `undefined` when the app is unlocked.
71
+ */
72
+ async getAppLock() {
73
+ const locks = await this.readAppLocks();
74
+ return locks[0]?.lockId;
75
+ }
76
+ /**
77
+ * Reads a state file.
78
+ *
79
+ * @throws {@link TransactionError} With the message `No state found!` when the file does not exist.
80
+ */
81
+ async getState(stateFileName) {
82
+ return this.localStateProvider.getState(stateFileName);
83
+ }
84
+ /**
85
+ * Whether `lockId` is the lock currently held.
86
+ *
87
+ * @throws {@link TransactionError} With the message `Invalid lock ID!` when a different lock is held,
88
+ * and when `lockId` is held but was neither taken nor resumed in this state directory.
89
+ */
90
+ async isAppLocked(lockId) {
91
+ const locks = await this.readAppLocks();
92
+ if (locks.some((lock) => lock.lockId === lockId)) {
93
+ const lockFile = await this.readLockFile();
94
+ if (lockFile.run?.lockId !== lockId) {
95
+ throw new TransactionError(`Invalid lock ID! Lock "${lockId}" was not taken in this state directory. Resume it here first.`);
96
+ }
97
+ return true;
98
+ }
99
+ if (locks.length > 0) {
100
+ throw new TransactionError('Invalid lock ID!');
101
+ }
102
+ return false;
103
+ }
104
+ /**
105
+ * The names of the state files whose name starts with `prefix`.
106
+ */
107
+ async listStates(prefix) {
108
+ return this.localStateProvider.listStates(prefix);
109
+ }
110
+ /**
111
+ * Takes the app lock and opens the run's record with everything the state directory holds.
112
+ *
113
+ * With `lockId`, resumes the run already holding that lock instead - from any machine, as the
114
+ * account that took it or an organization administrator. The state directory is replaced with
115
+ * what that run wrote so far, its journal included, keeping this machine's journal files, which join
116
+ * the record, and every write after lands in its record. A run that wrote nothing is resumed from the
117
+ * newest sealed state, without that state's journal.
118
+ *
119
+ * @throws {@link AppLockError} When a lock is held, naming the id that holds it. When the newest
120
+ * sealed state moved since {@link RemoteStateProvider.refreshState} last ran. When `lockId` is
121
+ * given and is not held.
122
+ */
123
+ async lockApp(lockId) {
124
+ if (lockId) {
125
+ return this.resumeAppLock(lockId);
126
+ }
127
+ return this.withUploadLock(async () => {
128
+ const lockFile = await this.readLockFile();
129
+ // Take the lock, naming a commit no run has used, since a sealed commit cannot be locked again.
130
+ const commitId = `${Date.now()}-${randomBytes(4).toString('hex')}`;
131
+ const response = await HttpUtility.sendJson(`${this.appStateUrl()}/lock`, {
132
+ body: { commitId, context: this.lockContext, scope: APP_LOCK_SCOPE },
133
+ headers: this.authorizationHeader(),
134
+ method: 'POST',
135
+ });
136
+ if (!response.ok) {
137
+ const message = await HttpUtility.readErrorMessage(response);
138
+ const [holder] = response.status === 409 ? await this.readAppLocks() : [];
139
+ if (holder) {
140
+ const heldFor = this.describeAge(holder.takenAt);
141
+ throw new AppLockError(`App already locked with lock ID "${holder.lockId}", taken by "${holder.context}"` +
142
+ `${heldFor ? `, held for ${heldFor}` : ''}!`, {
143
+ holder: {
144
+ commitId: holder.commitId,
145
+ context: holder.context,
146
+ lockId: holder.lockId,
147
+ takenAt: holder.takenAt,
148
+ },
149
+ reason: 'held',
150
+ });
151
+ }
152
+ throw new TransactionError(`Failed to lock the app: ${message}`);
153
+ }
154
+ const lock = (await response.json());
155
+ // Refuse a run on a state that moved since the directory was refreshed, and open its record.
156
+ // Nothing is sealed by releasing a lock whose record was never written.
157
+ try {
158
+ const newestState = await this.readNewestState();
159
+ if (newestState?.commitId !== lockFile.refreshedCommitId || (newestState && newestState.phase !== 'commit')) {
160
+ throw new AppLockError('App state moved since it was last refreshed! Run "octo generate" again.', {
161
+ newestCommitId: newestState?.commitId,
162
+ reason: 'state-moved',
163
+ refreshedCommitId: lockFile.refreshedCommitId,
164
+ });
165
+ }
166
+ const run = { commitId: lock.commitId, lockId: lock.lockId, stateFiles: {} };
167
+ run.stateFiles = await this.writeRunState(run);
168
+ await this.writeLockFile({ refreshedCommitId: lockFile.refreshedCommitId, run });
169
+ }
170
+ catch (error) {
171
+ await this.releaseAppLock(lock.lockId).catch(() => undefined);
172
+ throw error;
173
+ }
174
+ return { commitId: lock.commitId, lockId: lock.lockId };
175
+ });
176
+ }
177
+ /**
178
+ * Brings the state directory up to the newest sealed state, which is what a run plans against.
179
+ *
180
+ * A directory an open run is still writing is left alone. A directory the backend holds nothing
181
+ * for is left alone too: that is a first run, or a project arriving from another provider, and the
182
+ * files on disk are the only copy there is. Journal files are neither replaced nor taken.
183
+ */
184
+ async refreshState() {
185
+ return this.withUploadLock(async () => {
186
+ const lockFile = await this.readLockFile();
187
+ // Keep the directory of a run whose lock is still held.
188
+ if (lockFile.run) {
189
+ const locks = await this.readAppLocks();
190
+ if (locks.some((lock) => lock.lockId === lockFile.run.lockId)) {
191
+ return { replaced: false };
192
+ }
193
+ }
194
+ // Skip the download when the directory already holds the newest sealed state.
195
+ const newestState = await this.readNewestState();
196
+ if (newestState?.phase === 'commit' && newestState.commitId === lockFile.refreshedCommitId) {
197
+ await this.writeLockFile({ refreshedCommitId: lockFile.refreshedCommitId });
198
+ return { commitId: lockFile.refreshedCommitId, replaced: false };
199
+ }
200
+ const response = await HttpUtility.send(`${this.appStateUrl()}/latest`, {
201
+ headers: this.authorizationHeader(),
202
+ });
203
+ if (response.status === 404) {
204
+ await this.deleteLockFile();
205
+ return { replaced: false };
206
+ }
207
+ if (!response.ok) {
208
+ throw new TransactionError(`Failed to read the app state: ${await HttpUtility.readErrorMessage(response)}`);
209
+ }
210
+ const { state } = await this.replaceStateFiles(response, { takeJournal: false });
211
+ await this.writeLockFile({ refreshedCommitId: state.commitId });
212
+ return { commitId: state.commitId, previousCommitId: lockFile.refreshedCommitId, replaced: true };
213
+ });
214
+ }
215
+ /**
216
+ * Writes state files, deleting each one given no `data`, and then writes the run's record once for
217
+ * all of them when a run is open. A name holding `/` is the machine's own: it is written beneath
218
+ * the state directory and never reaches the record.
219
+ *
220
+ * @throws {@link TransactionError} When the backend would refuse a file's name, before anything is
221
+ * written. When the backend refuses the write, which is where the limits it holds the app to apply.
222
+ */
223
+ async saveStates(stateFiles) {
224
+ // Refuse the whole change before any of it lands on disk.
225
+ for (const stateFile of stateFiles) {
226
+ const { stateFileName } = stateFile;
227
+ if (stateFileName.includes('/') || ('data' in stateFile && stateFile.data === undefined)) {
228
+ continue;
229
+ }
230
+ if (stateFileName.length > STATE_FILE_NAME_MAX_LENGTH ||
231
+ !STATE_FILE_NAME_PATTERN.test(stateFileName) ||
232
+ !STATE_FILE_EXTENSIONS.includes(extname(stateFileName))) {
233
+ throw new TransactionError(`State file name "${stateFileName}" is not accepted by the remote state!`);
234
+ }
235
+ }
236
+ // Write the directory, then the run's record once.
237
+ for (const stateFile of stateFiles) {
238
+ if ('filePath' in stateFile) {
239
+ await this.writeStateFile(stateFile.stateFileName, { filePath: stateFile.filePath });
240
+ }
241
+ else if (stateFile.data === undefined) {
242
+ await rm(join(this.localStateDirectoryPath, stateFile.stateFileName), { force: true });
243
+ }
244
+ else {
245
+ await this.writeStateFile(stateFile.stateFileName, { data: stateFile.data });
246
+ }
247
+ }
248
+ if (stateFiles.some(({ stateFileName }) => !stateFileName.includes('/'))) {
249
+ await this.pushRun();
250
+ }
251
+ }
252
+ /**
253
+ * Writes the directory into the run's record, seals it, and releases the app lock. Unlocking an app
254
+ * that is not locked is not an error.
255
+ *
256
+ * A write that failed earlier left the directory ahead of the record, so the record is written once
257
+ * more before it is sealed: a sealed record is what every machine plans against next.
258
+ *
259
+ * @throws {@link TransactionError} With the message `Invalid lock ID!` when a different lock is held.
260
+ * When the directory cannot be written into the record, keeping the lock held.
261
+ */
262
+ async unlockApp(lockId) {
263
+ await this.withUploadLock(async () => {
264
+ const lockFile = await this.readLockFile();
265
+ const run = lockFile.run?.lockId === lockId ? lockFile.run : undefined;
266
+ // Bring the record up to the directory, when this directory's run still holds the lock.
267
+ if (run && (await this.readAppLocks()).some((lock) => lock.lockId === lockId)) {
268
+ await this.writeRunState(run);
269
+ }
270
+ const released = await this.releaseAppLock(lockId);
271
+ if (!released && (await this.readAppLocks()).length > 0) {
272
+ throw new TransactionError('Invalid lock ID!');
273
+ }
274
+ // Drop the lock file of the run this directory belonged to.
275
+ if (run) {
276
+ await this.deleteLockFile();
277
+ }
278
+ });
279
+ }
280
+ /**
281
+ * The route this app's state and locks are read and written at.
282
+ */
283
+ appStateUrl() {
284
+ return `${this.stateManagementServiceUrl}/organization/${this.organizationId}/app/${this.appId}`;
285
+ }
286
+ /**
287
+ * The session this run acts as. Every route but the health check is behind it.
288
+ */
289
+ authorizationHeader() {
290
+ return { Authorization: `Bearer ${this.sessionId}` };
291
+ }
292
+ /**
293
+ * Deletes the lock file. Deleting one that is not there is not an error.
294
+ */
295
+ async deleteLockFile() {
296
+ await rm(join(this.localStateDirectoryPath, LOCK_FILE_NAME), { force: true });
297
+ }
298
+ /**
299
+ * How long the lock has been held, as a short readable span. Undefined when the recorded time
300
+ * cannot be read as one, so a broken value is left out rather than reported as a negative age.
301
+ */
302
+ describeAge(takenAt) {
303
+ const heldForMs = Date.now() - takenAt;
304
+ if (!Number.isFinite(heldForMs) || heldForMs < 0) {
305
+ return undefined;
306
+ }
307
+ const seconds = Math.floor(heldForMs / 1000);
308
+ if (seconds < 60) {
309
+ return `${seconds}s`;
310
+ }
311
+ const minutes = Math.floor(seconds / 60);
312
+ if (minutes < 60) {
313
+ return `${minutes}m`;
314
+ }
315
+ const hours = Math.floor(minutes / 60);
316
+ if (hours < 24) {
317
+ return `${hours}h ${minutes % 60}m`;
318
+ }
319
+ return `${Math.floor(hours / 24)}d ${hours % 24}h`;
320
+ }
321
+ /**
322
+ * Writes the directory into the run's record, when this directory belongs to an open run.
323
+ */
324
+ async pushRun() {
325
+ if (!(await this.readLockFile()).run) {
326
+ return;
327
+ }
328
+ await this.withUploadLock(async () => {
329
+ const lockFile = await this.readLockFile();
330
+ if (lockFile.run) {
331
+ lockFile.run.stateFiles = await this.writeRunState(lockFile.run);
332
+ await this.writeLockFile(lockFile);
333
+ }
334
+ });
335
+ }
336
+ /**
337
+ * Every lock the backend holds on this app, oldest first.
338
+ */
339
+ async readAppLocks() {
340
+ const response = await HttpUtility.send(`${this.appStateUrl()}/locks`, { headers: this.authorizationHeader() });
341
+ if (!response.ok) {
342
+ throw new TransactionError(`Failed to read the app lock: ${await HttpUtility.readErrorMessage(response)}`);
343
+ }
344
+ return (await response.json()).locks;
345
+ }
346
+ /**
347
+ * The lock file. Empty when there is none.
348
+ */
349
+ async readLockFile() {
350
+ try {
351
+ return JSON.parse(await readFile(join(this.localStateDirectoryPath, LOCK_FILE_NAME), 'utf-8'));
352
+ }
353
+ catch (error) {
354
+ if (error.code === 'ENOENT') {
355
+ return {};
356
+ }
357
+ throw error;
358
+ }
359
+ }
360
+ /**
361
+ * The newest record the app holds, sealed or not, or `undefined` when it holds none.
362
+ *
363
+ * One run holds the whole app at a time, so records are opened in the order they are sealed.
364
+ */
365
+ async readNewestState() {
366
+ const response = await HttpUtility.send(`${this.appStateUrl()}?limit=1`, { headers: this.authorizationHeader() });
367
+ if (!response.ok) {
368
+ throw new TransactionError(`Failed to read the app state: ${await HttpUtility.readErrorMessage(response)}`);
369
+ }
370
+ return (await response.json()).states[0];
371
+ }
372
+ /**
373
+ * Releases a lock, sealing the record its commit names. False when the lock is not held.
374
+ */
375
+ async releaseAppLock(lockId) {
376
+ const response = await HttpUtility.send(`${this.appStateUrl()}/lock/${lockId}/unlock`, {
377
+ headers: this.authorizationHeader(),
378
+ method: 'POST',
379
+ });
380
+ if (response.ok) {
381
+ return true;
382
+ }
383
+ if (response.status === 404) {
384
+ return false;
385
+ }
386
+ throw new TransactionError(`Failed to unlock the app: ${await HttpUtility.readErrorMessage(response)}`);
387
+ }
388
+ /**
389
+ * Replaces every state file in the directory with the ones a state body names, downloading each
390
+ * from its URL and checking it against its SHA-256, and returns the body. The directory's journal
391
+ * files are kept; with `takeJournal`, the body's are written beside them.
392
+ *
393
+ * Each file is streamed to a temporary file beside it, so a state is not bounded by memory.
394
+ */
395
+ async replaceStateFiles(response, { takeJournal }) {
396
+ const stateFiles = (await response.json());
397
+ const isJournal = (stateFileName) => stateFileName.startsWith(JOURNAL_STATE_FILE_NAME_PREFIX);
398
+ // Download every file before the directory changes, so a failed download leaves it as it was.
399
+ await FileUtility.ensureDirectory(this.localStateDirectoryPath);
400
+ const downloads = [];
401
+ try {
402
+ for (const stateFile of stateFiles.stateFiles.filter((f) => takeJournal || !isJournal(f.filename))) {
403
+ const temporaryFilePath = join(this.localStateDirectoryPath, `.${stateFile.filename}.${process.pid}.download`);
404
+ downloads.push({ stateFileName: stateFile.filename, temporaryFilePath });
405
+ if (!(await HttpUtility.downloadToFile(stateFile.downloadUrl, temporaryFilePath))) {
406
+ throw new TransactionError(`Failed to read the app state: state file "${stateFile.filename}" did not download!`);
407
+ }
408
+ if ((await FileUtility.getSha256Digest(temporaryFilePath)) !== stateFile.sha256) {
409
+ throw new TransactionError(`Failed to read the app state: state file "${stateFile.filename}" did not download whole!`);
410
+ }
411
+ }
412
+ }
413
+ catch (error) {
414
+ for (const { temporaryFilePath } of downloads) {
415
+ await rm(temporaryFilePath, { force: true });
416
+ }
417
+ throw error;
418
+ }
419
+ // Take the state the backend holds over whatever the directory held, file for file.
420
+ for (const stateFileName of (await this.stateFileNames()).filter((n) => !isJournal(n))) {
421
+ await rm(join(this.localStateDirectoryPath, stateFileName), { force: true });
422
+ }
423
+ for (const { stateFileName, temporaryFilePath } of downloads) {
424
+ await rename(temporaryFilePath, join(this.localStateDirectoryPath, stateFileName));
425
+ }
426
+ return stateFiles;
427
+ }
428
+ /**
429
+ * Resumes the run holding `lockId` in this state directory.
430
+ */
431
+ async resumeAppLock(lockId) {
432
+ return this.withUploadLock(async () => {
433
+ const lock = (await this.readAppLocks()).find((l) => l.lockId === lockId);
434
+ if (!lock) {
435
+ throw new AppLockError(`App is not locked with lock ID "${lockId}"!`, { reason: 'not-held' });
436
+ }
437
+ const lockFile = await this.readLockFile();
438
+ // Continue in the directory the run was taken in, which holds everything it wrote.
439
+ if (lockFile.run?.lockId === lockId) {
440
+ lockFile.run.stateFiles = await this.writeRunState(lockFile.run);
441
+ await this.writeLockFile(lockFile);
442
+ return { commitId: lock.commitId, lockId, resumedFrom: 'directory' };
443
+ }
444
+ // Read what the run wrote so far. A run that wrote nothing started from the newest sealed state,
445
+ // which is read as a refresh reads it, without its journal.
446
+ let response = await HttpUtility.send(`${this.appStateUrl()}/commit/${lock.commitId}/files`, {
447
+ headers: this.authorizationHeader(),
448
+ });
449
+ const isRunWritten = response.status !== 404;
450
+ if (!isRunWritten) {
451
+ response = await HttpUtility.send(`${this.appStateUrl()}/latest`, { headers: this.authorizationHeader() });
452
+ }
453
+ if (!response.ok && response.status !== 404) {
454
+ throw new TransactionError(`Failed to read the app state: ${await HttpUtility.readErrorMessage(response)}`);
455
+ }
456
+ const run = { commitId: lock.commitId, lockId, stateFiles: {} };
457
+ if (response.ok) {
458
+ const { state, stateFiles } = await this.replaceStateFiles(response, { takeJournal: isRunWritten });
459
+ if (state.commitId === lock.commitId) {
460
+ for (const stateFile of stateFiles) {
461
+ run.stateFiles[stateFile.filename] = stateFile.sha256;
462
+ }
463
+ }
464
+ }
465
+ run.stateFiles = await this.writeRunState(run);
466
+ await this.writeLockFile({ refreshedCommitId: lockFile.refreshedCommitId, run });
467
+ return { commitId: lock.commitId, lockId, resumedFrom: isRunWritten ? 'run' : 'sealed' };
468
+ });
469
+ }
470
+ /**
471
+ * Every state file the directory holds, which is every file in it but the provider's two lock
472
+ * files and a write still in progress, and none of the directories a run writes beside them, such
473
+ * as a generated terragrunt folder.
474
+ */
475
+ async stateFileNames() {
476
+ try {
477
+ return (await readdir(this.localStateDirectoryPath, { withFileTypes: true }))
478
+ .filter((entry) => entry.isFile() &&
479
+ !entry.name.startsWith('.') &&
480
+ entry.name !== LOCK_FILE_NAME &&
481
+ entry.name !== UPLOAD_LOCK_FILE_NAME)
482
+ .map((entry) => entry.name);
483
+ }
484
+ catch (error) {
485
+ if (error.code === 'ENOENT') {
486
+ return [];
487
+ }
488
+ throw error;
489
+ }
490
+ }
491
+ /**
492
+ * Runs `body` while holding `remote-state-upload.lock`, so one write to the backend is in flight at
493
+ * a time across every octo process on this machine. The file is deleted when `body` returns, and
494
+ * one left by a process that is gone is taken over.
495
+ */
496
+ async withUploadLock(body) {
497
+ await FileUtility.ensureDirectory(this.localStateDirectoryPath);
498
+ const uploadLockFilePath = join(this.localStateDirectoryPath, UPLOAD_LOCK_FILE_NAME);
499
+ // Take the upload lock, waiting on a live holder.
500
+ while (true) {
501
+ try {
502
+ await writeFile(uploadLockFilePath, String(process.pid), { flag: 'wx' });
503
+ break;
504
+ }
505
+ catch (error) {
506
+ if (error.code !== 'EEXIST') {
507
+ throw error;
508
+ }
509
+ }
510
+ const holderProcessId = Number(await readFile(uploadLockFilePath, 'utf-8').catch(() => ''));
511
+ let isHolderAlive = !holderProcessId;
512
+ if (holderProcessId) {
513
+ try {
514
+ process.kill(holderProcessId, 0);
515
+ isHolderAlive = true;
516
+ }
517
+ catch (error) {
518
+ isHolderAlive = error.code === 'EPERM';
519
+ }
520
+ }
521
+ if (isHolderAlive) {
522
+ await new Promise((resolveTimeout) => setTimeout(resolveTimeout, UPLOAD_LOCK_RETRY_IN_MS));
523
+ }
524
+ else {
525
+ await rm(uploadLockFilePath, { force: true });
526
+ }
527
+ }
528
+ try {
529
+ return await body();
530
+ }
531
+ finally {
532
+ await rm(uploadLockFilePath, { force: true });
533
+ }
534
+ }
535
+ /**
536
+ * Writes the lock file.
537
+ */
538
+ async writeLockFile(lockFile) {
539
+ await this.writeStateFile(LOCK_FILE_NAME, { data: Buffer.from(JSON.stringify(lockFile, null, 2)) });
540
+ }
541
+ /**
542
+ * Writes the directory into the run's record as it stands, and returns the SHA-256 of every file the
543
+ * record holds afterwards.
544
+ *
545
+ * A write is three requests: the upload is asked for naming every file and its digest, the files
546
+ * the backend does not already hold are PUT to the URLs it answered with, and the upload is
547
+ * claimed. A file that fails to upload stops the write before the claim, and a claim with a file
548
+ * missing is refused, so a write lands whole or not at all. An upload left unclaimed is thrown away by
549
+ * the backend at the next write or when the lock is released. A directory holding nothing writes
550
+ * nothing, since the backend holds no empty state.
551
+ *
552
+ * The files are hard linked into a snapshot directory first and streamed from there, so a state is
553
+ * not bounded by memory, and a file another process replaces meanwhile is uploaded as it was
554
+ * hashed: a write replaces a file by renaming over it, which leaves the linked one as it was.
555
+ */
556
+ async writeRunState(run) {
557
+ const stateFileNames = await this.stateFileNames();
558
+ if (stateFileNames.length === 0) {
559
+ return run.stateFiles;
560
+ }
561
+ const snapshotDirectoryPath = join(this.localStateDirectoryPath, `.remote-state-snapshot.${process.pid}`);
562
+ await rm(snapshotDirectoryPath, { force: true, recursive: true });
563
+ await FileUtility.ensureDirectory(snapshotDirectoryPath);
564
+ try {
565
+ // Describe every file, and stop where nothing changed since the record was last written.
566
+ const stateFiles = [];
567
+ for (const stateFileName of stateFileNames) {
568
+ const filePath = join(snapshotDirectoryPath, stateFileName);
569
+ try {
570
+ await link(join(this.localStateDirectoryPath, stateFileName), filePath);
571
+ }
572
+ catch (error) {
573
+ // Skip a file deleted since the directory was listed.
574
+ if (error.code === 'ENOENT') {
575
+ continue;
576
+ }
577
+ throw error;
578
+ }
579
+ stateFiles.push({
580
+ filename: stateFileName,
581
+ filePath,
582
+ sha256: await FileUtility.getSha256Digest(filePath),
583
+ sizeInBytes: (await stat(filePath)).size,
584
+ });
585
+ }
586
+ if (stateFiles.length === Object.keys(run.stateFiles).length &&
587
+ stateFiles.every((stateFile) => run.stateFiles[stateFile.filename] === stateFile.sha256)) {
588
+ return run.stateFiles;
589
+ }
590
+ // Ask for the upload, naming every file the record should hold.
591
+ const lockUrl = `${this.appStateUrl()}/lock/${run.lockId}`;
592
+ const created = await HttpUtility.sendJson(`${lockUrl}/upload`, {
593
+ body: {
594
+ stateFiles: stateFiles.map(({ filename, sha256, sizeInBytes }) => ({ filename, sha256, sizeInBytes })),
595
+ },
596
+ headers: this.authorizationHeader(),
597
+ method: 'POST',
598
+ });
599
+ if (created.status === 404) {
600
+ throw new TransactionError(`App is not locked with lock ID "${run.lockId}"!`);
601
+ }
602
+ if (!created.ok) {
603
+ throw new TransactionError(`Failed to write the app state: ${await HttpUtility.readErrorMessage(created)}`);
604
+ }
605
+ const { uploadId, uploads } = (await created.json());
606
+ // Upload each file the backend asked for, with the headers its URL was signed with.
607
+ for (const upload of uploads) {
608
+ const response = await HttpUtility.uploadFile(upload.url, stateFiles.find((f) => f.filename === upload.filename).filePath, Object.fromEntries(upload.headers.map((header) => [header.name, header.value])));
609
+ if (!response.ok) {
610
+ throw new TransactionError(`Failed to write the app state: state file "${upload.filename}" did not upload!`);
611
+ }
612
+ }
613
+ // Claim the upload, which the backend refuses and throws away where a file is missing.
614
+ const claimed = await HttpUtility.send(`${lockUrl}/upload/${uploadId}/claim`, {
615
+ headers: this.authorizationHeader(),
616
+ method: 'POST',
617
+ });
618
+ if (!claimed.ok) {
619
+ throw new TransactionError(`Failed to write the app state: ${await HttpUtility.readErrorMessage(claimed)}`);
620
+ }
621
+ return Object.fromEntries(stateFiles.map((stateFile) => [stateFile.filename, stateFile.sha256]));
622
+ }
623
+ finally {
624
+ await rm(snapshotDirectoryPath, { force: true, recursive: true });
625
+ }
626
+ }
627
+ /**
628
+ * Writes one file into the state directory whole, from `data` or copied from `filePath`, so a process
629
+ * reading it never reads half of it. Replacing the file rather than writing into it is also what lets
630
+ * an upload freeze it with a hard link.
631
+ */
632
+ async writeStateFile(stateFileName, source) {
633
+ const stateFilePath = join(this.localStateDirectoryPath, stateFileName);
634
+ await FileUtility.ensureDirectory(dirname(stateFilePath));
635
+ const temporaryFilePath = join(dirname(stateFilePath), `.${basename(stateFilePath)}.${process.pid}.tmp`);
636
+ if ('filePath' in source) {
637
+ await copyFile(source.filePath, temporaryFilePath);
638
+ }
639
+ else {
640
+ await writeFile(temporaryFilePath, source.data);
641
+ }
642
+ await rename(temporaryFilePath, stateFilePath);
643
+ }
644
+ }
645
+ //# sourceMappingURL=remote.state-provider.js.map