@starci/hfs 4.7.31 → 4.7.33

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 (473) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -0,0 +1,32 @@
1
+ -- The outbox of the queues: a producer's business transaction writes one row here, so the job exists exactly when
2
+ -- the change commits; the relay hands each unsent row to BullMQ on the declared Redis with the row id as the job id
3
+ -- and the correlation id of the request that wrote it.
4
+ create table public.queue (
5
+ id uuid primary key default gen_random_uuid(),
6
+ queue varchar(200) not null,
7
+ payload jsonb not null,
8
+ created_at timestamptz not null,
9
+ sent_at timestamptz,
10
+ correlation_id varchar(128) not null
11
+ );
12
+
13
+ create index queue_unsent_idx on public.queue (created_at, id) where sent_at is null;
14
+
15
+ alter table public.queue enable row level security;
16
+ alter table public.queue force row level security;
17
+
18
+ create policy queue_app_be_select
19
+ on public.queue for select to app_be
20
+ using (queue is not null);
21
+
22
+ create policy queue_app_be_insert
23
+ on public.queue for insert to app_be
24
+ with check (queue is not null);
25
+
26
+ create policy queue_app_be_update
27
+ on public.queue for update to app_be
28
+ using (queue is not null)
29
+ with check (queue is not null);
30
+
31
+ revoke all on table public.queue from public, anon, authenticated;
32
+ grant select, insert, update on table public.queue to app_be;
@@ -0,0 +1,148 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { FakeClock, fakeTransaction, mock, mockEntityManager } from "@starci/jest-preset"
3
+ import type { MockEntityManager } from "@starci/jest-preset"
4
+ import { CLOCK } from "@modules/platform/clock"
5
+ import { LOGGER } from "@modules/platform/logging"
6
+ import type { Logger } from "@modules/platform/logging"
7
+ import { QUEUE_OPTIONS, QUEUE_RELAY_MANAGERS, QUEUE_TRANSPORT } from "./queue.decorators"
8
+ import { OutboxLogEvent } from "@modules/platform/outbox"
9
+ import type { QueueOptions } from "./queue.options"
10
+ import { QueueRelayService } from "./queue-relay.service"
11
+ import type { QueueTransport } from "./queue-transport.port"
12
+ import { MARK_ROWS_SENT, SELECT_WAITING_ROWS } from "./persistence/queue.sql"
13
+
14
+ const AT = "2026-02-03T04:05:06.000Z"
15
+
16
+ const options: QueueOptions = {
17
+ redisHost: "localhost",
18
+ redisPort: 6379,
19
+ redisDb: 0,
20
+ prefix: "test",
21
+ relayIntervalMs: 100,
22
+ relayBatch: 2,
23
+ concurrency: 1,
24
+ connections: [],
25
+ schedulers: [],
26
+ }
27
+
28
+ const row = (id: string) => ({ id, queue: "mail", payload: { id: `m-${id}` }, correlationId: `request-${id}` })
29
+
30
+ const build = async (manager: MockEntityManager) => {
31
+ const transport = mock<QueueTransport>()
32
+ const logger = mock<Logger>()
33
+ const moduleRef = await Test.createTestingModule({
34
+ providers: [
35
+ QueueRelayService,
36
+ { provide: QUEUE_OPTIONS, useValue: options },
37
+ { provide: QUEUE_RELAY_MANAGERS, useValue: [manager] },
38
+ { provide: QUEUE_TRANSPORT, useValue: transport },
39
+ { provide: CLOCK, useValue: new FakeClock(AT) },
40
+ { provide: LOGGER, useValue: logger },
41
+ ],
42
+ }).compile()
43
+ return { relay: moduleRef.get(QueueRelayService), transport, logger }
44
+ }
45
+
46
+ describe("QueueRelayService relay", () => {
47
+ it("adds the waiting rows to BullMQ with the row id as the job id and marks them sent once Redis has persisted them", async () => {
48
+ const tx = fakeTransaction(
49
+ mockEntityManager({
50
+ query: [
51
+ [SELECT_WAITING_ROWS, [row("1"), row("2")]],
52
+ [MARK_ROWS_SENT, []],
53
+ ],
54
+ }),
55
+ )
56
+ const { relay, transport } = await build(tx.em)
57
+
58
+ await expect(relay.relay()).resolves.toBe(2)
59
+
60
+ expect(tx.em.query).toHaveBeenNthCalledWith(1, SELECT_WAITING_ROWS, [2])
61
+ expect(transport.add).toHaveBeenNthCalledWith(1, "mail", "1", { id: "m-1" }, "request-1")
62
+ expect(transport.add).toHaveBeenNthCalledWith(2, "mail", "2", { id: "m-2" }, "request-2")
63
+ expect(transport.persist).toHaveBeenCalledWith([
64
+ { queue: "mail", jobId: "1" },
65
+ { queue: "mail", jobId: "2" },
66
+ ])
67
+ expect(transport.persist.mock.invocationCallOrder[0]).toBeGreaterThan(
68
+ transport.add.mock.invocationCallOrder[1] ?? 0,
69
+ )
70
+ expect(tx.em.query).toHaveBeenNthCalledWith(2, MARK_ROWS_SENT, [["1", "2"], new Date(AT)])
71
+ expect(tx.commits).toBe(1)
72
+ })
73
+
74
+ it("leaves the rows waiting when Redis has not persisted their jobs, so the next pass adds them again", async () => {
75
+ const tx = fakeTransaction(mockEntityManager({ query: [SELECT_WAITING_ROWS, [row("1")]] }))
76
+ const { relay, transport } = await build(tx.em)
77
+ const lost = new Error("job lost in a Redis restart")
78
+ transport.persist.mockRejectedValue(lost)
79
+
80
+ await expect(relay.relay()).rejects.toBe(lost)
81
+
82
+ expect(transport.add).toHaveBeenCalledWith("mail", "1", { id: "m-1" }, "request-1")
83
+ expect(tx.rollbacks).toBe(1)
84
+ expect(tx.em.query).toHaveBeenCalledTimes(1)
85
+ })
86
+
87
+ it("adds nothing and marks nothing when no row waits", async () => {
88
+ const tx = fakeTransaction(mockEntityManager({ query: [SELECT_WAITING_ROWS, []] }))
89
+ const { relay, transport } = await build(tx.em)
90
+
91
+ await expect(relay.relay()).resolves.toBe(0)
92
+
93
+ expect(transport.add).not.toHaveBeenCalled()
94
+ expect(transport.persist).not.toHaveBeenCalled()
95
+ expect(tx.em.query).toHaveBeenCalledTimes(1)
96
+ })
97
+
98
+ it("leaves the rows unmarked when BullMQ refuses, so the next pass adds them again", async () => {
99
+ const tx = fakeTransaction(mockEntityManager({ query: [SELECT_WAITING_ROWS, [row("1")]] }))
100
+ const { relay, transport } = await build(tx.em)
101
+ const failure = new Error("redis down")
102
+ transport.add.mockRejectedValue(failure)
103
+
104
+ await expect(relay.relay()).rejects.toBe(failure)
105
+
106
+ expect(tx.rollbacks).toBe(1)
107
+ expect(tx.em.query).toHaveBeenCalledTimes(1)
108
+ })
109
+ })
110
+
111
+ describe("QueueRelayService the loop", () => {
112
+ it("passes again at once after a pass that added rows, pauses on an empty outbox, logs a failed pass and stops on shutdown", async () => {
113
+ const tx = fakeTransaction(
114
+ mockEntityManager({
115
+ query: [
116
+ [SELECT_WAITING_ROWS, [row("1")]],
117
+ [MARK_ROWS_SENT, []],
118
+ [SELECT_WAITING_ROWS, []],
119
+ ],
120
+ }),
121
+ )
122
+ const { relay, transport, logger } = await build(tx.em)
123
+ const failure = new Error("outbox unreadable")
124
+ let waits = 0
125
+ let shutdown: Promise<void> = Promise.resolve()
126
+ let reachedSecondWait: () => void = () => undefined
127
+ const secondWait = new Promise<void>((resolve) => {
128
+ reachedSecondWait = resolve
129
+ })
130
+ transport.wait.mockImplementation(() => {
131
+ waits += 1
132
+ if (waits === 1) tx.em.query.mockRejectedValueOnce(failure)
133
+ else {
134
+ shutdown = relay.onApplicationShutdown()
135
+ reachedSecondWait()
136
+ }
137
+ return Promise.resolve()
138
+ })
139
+
140
+ relay.onApplicationBootstrap()
141
+ await secondWait
142
+ await shutdown
143
+
144
+ expect(transport.add).toHaveBeenCalledTimes(1)
145
+ expect(transport.wait).toHaveBeenCalledWith(100)
146
+ expect(logger.error).toHaveBeenCalledWith(OutboxLogEvent.RelayFailed, failure, { outbox: "queue" })
147
+ })
148
+ })
@@ -0,0 +1,51 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { EntityManager } from "typeorm"
3
+ import { InjectClock } from "@modules/platform/clock"
4
+ import type { Clock } from "@modules/platform/clock"
5
+ import { InjectLogger } from "@modules/platform/logging"
6
+ import type { Logger } from "@modules/platform/logging"
7
+ import { eachInOrder } from "@modules/platform/primitives"
8
+ import { OutboxRelayPolicy } from "@modules/platform/outbox"
9
+ import { InjectQueueOptions, InjectQueueRelayManagers, InjectQueueTransport } from "./queue.decorators"
10
+ import type { QueueOptions } from "./queue.options"
11
+ import type { QueueTransport } from "./queue-transport.port"
12
+ import { MARK_ROWS_SENT, SELECT_WAITING_ROWS } from "./persistence/queue.sql"
13
+ import type { QueueRow } from "./persistence/queue.rows"
14
+
15
+ @Injectable()
16
+ /**
17
+ * The relay of the queue outbox: it adds each waiting row to BullMQ with the row id as the job id, waits until Redis has written
18
+ * those jobs to its append-only file, and only then marks the rows sent in the same transaction (the loop is `OutboxRelayPolicy`'s).
19
+ * A job Redis loses before that write leaves its row waiting, so the next pass adds it again; a crash between the add and the mark
20
+ * does the same, which BullMQ ignores while it holds the id and the job fence absorbs afterwards.
21
+ */
22
+ export class QueueRelayService extends OutboxRelayPolicy<QueueRow> {
23
+ protected readonly outbox = "queue"
24
+
25
+ constructor(
26
+ @InjectQueueOptions() private readonly options: QueueOptions,
27
+ @InjectQueueRelayManagers() protected readonly managers: ReadonlyArray<EntityManager>,
28
+ @InjectQueueTransport() private readonly transport: QueueTransport,
29
+ @InjectClock() private readonly clock: Clock,
30
+ @InjectLogger() protected readonly logger: Logger,
31
+ ) {
32
+ super()
33
+ }
34
+
35
+ protected select(tx: EntityManager): Promise<Array<QueueRow>> {
36
+ return tx.query(SELECT_WAITING_ROWS, [this.options.relayBatch])
37
+ }
38
+
39
+ protected async deliver(rows: ReadonlyArray<QueueRow>): Promise<void> {
40
+ await eachInOrder(rows, (row) => this.transport.add(row.queue, row.id, row.payload, row.correlationId))
41
+ await this.transport.persist(rows.map((row) => ({ queue: row.queue, jobId: row.id })))
42
+ }
43
+
44
+ protected async mark(tx: EntityManager, rows: ReadonlyArray<QueueRow>): Promise<void> {
45
+ await tx.query(MARK_ROWS_SENT, [rows.map((row) => row.id), this.clock.now()])
46
+ }
47
+
48
+ protected idle(): Promise<void> {
49
+ return this.transport.wait(this.options.relayIntervalMs)
50
+ }
51
+ }
@@ -0,0 +1,163 @@
1
+ import type { BULLMQ_JOB_SCHEMA, QueueHandler, QueueSchedulerDefinition } from "./queue.contracts"
2
+
3
+ /** A job the relay added: its queue and its id, the outbox row id. */
4
+ export interface QueuedJob {
5
+ /** The queue the job was added to. */
6
+ readonly queue: string
7
+ /** The job id, which is the outbox row id. */
8
+ readonly jobId: string
9
+ }
10
+
11
+ /** BullMQ as the queue capability sees it: the one port the BullMQ client implements. */
12
+ export interface QueueTransport {
13
+ /** Adds the job to its queue with `jobId`; adding an id BullMQ already holds changes nothing, so a repeated relay pass is harmless. */
14
+ add(queue: string, jobId: string, payload: object, correlationId: string): Promise<void>
15
+ /**
16
+ * Resolves once Redis has written the jobs to its append-only file and still holds each of them; it throws otherwise, so the
17
+ * relay marks an outbox row sent only when its job survives a Redis crash.
18
+ */
19
+ persist(jobs: ReadonlyArray<QueuedJob>): Promise<void>
20
+ /** Creates or updates a job scheduler by its id. */
21
+ upsertScheduler(scheduler: QueueSchedulerDefinition): Promise<void>
22
+ /** Starts a worker of the queue the handler names that runs it for every job, `concurrency` at a time. */
23
+ work(handler: QueueHandler, concurrency: number): Promise<void>
24
+ /** Resolves after `ms` milliseconds: the only clock of the relay loop. */
25
+ wait(ms: number): Promise<void>
26
+ /** Closes every queue and worker. */
27
+ close(): Promise<void>
28
+ }
29
+
30
+ /** The Redis a driver object connects to. */
31
+ export interface BullmqRedis {
32
+ /** The host. */
33
+ readonly host: string
34
+ /** The port. */
35
+ readonly port: number
36
+ /** The Redis database that holds this app's queue state. */
37
+ readonly db: number
38
+ /** The password of the declared Redis; absent for a local Redis without auth. */
39
+ readonly password?: string
40
+ /** BullMQ answers to commands only through a client that does not retry on its own. */
41
+ readonly maxRetriesPerRequest: number | null
42
+ }
43
+
44
+ /** The Redis options every queue and worker of the app shares. */
45
+ export interface BullmqConnection {
46
+ /** The key prefix of the app. */
47
+ readonly prefix: string
48
+ /** The Redis the driver connects to. */
49
+ readonly connection: BullmqRedis
50
+ }
51
+
52
+ /** The options a worker is built with: the shared connection plus its concurrency. */
53
+ export interface BullmqWorkerOptions extends BullmqConnection {
54
+ /** How many jobs the worker runs at a time. */
55
+ readonly concurrency: number
56
+ }
57
+
58
+ /** The wait between two tries of a job. */
59
+ export interface BullmqBackoff {
60
+ /** The backoff shape. */
61
+ readonly type: string
62
+ /** The first wait, in milliseconds. */
63
+ readonly delay: number
64
+ }
65
+
66
+ /** How long a finished job is kept. */
67
+ export interface BullmqRetention {
68
+ /** The seconds a finished job is kept. */
69
+ readonly age: number
70
+ }
71
+
72
+ /** The options the transport fixes on every job it adds. */
73
+ export interface BullmqJobOptions {
74
+ /** The id BullMQ dedupes on: the outbox row id. */
75
+ readonly jobId: string
76
+ /** How many times BullMQ tries the job. */
77
+ readonly attempts: number
78
+ /** The wait between two tries. */
79
+ readonly backoff: BullmqBackoff
80
+ /** How long a finished job is kept. */
81
+ readonly removeOnComplete: BullmqRetention
82
+ }
83
+
84
+ /** The options a scheduler fixes on the jobs it fires. */
85
+ export interface BullmqSchedulerJobOptions {
86
+ /** How many times BullMQ tries the tick. */
87
+ readonly attempts: number
88
+ }
89
+
90
+ /** The job a scheduler fires. */
91
+ export interface BullmqJobTemplate {
92
+ /** The job name, which is the queue name. */
93
+ readonly name: string
94
+ /** The payload of every tick. */
95
+ readonly data: BullmqJobData
96
+ /** The options of every tick. */
97
+ readonly opts: BullmqSchedulerJobOptions
98
+ }
99
+
100
+ /** A Redis connection that sends a raw command: BullMQ's ioredis client forwards `call` to ioredis. */
101
+ export interface BullmqCommands {
102
+ /** Sends one command with its arguments and answers its reply. */
103
+ call(command: string, ...args: Array<string | number>): Promise<unknown>
104
+ }
105
+
106
+ /** The Redis backend of a queue: it owns the connection the queue adds its jobs through. */
107
+ export interface BullmqBackend {
108
+ /** The connection the queue adds its jobs through; BullMQ types it without its raw `call`. */
109
+ readonly client: Promise<object>
110
+ }
111
+
112
+ /** A BullMQ driver whose infrastructure failures must use the application logger. */
113
+ export interface BullmqErrorSource {
114
+ /** Subscribes before asynchronous driver errors can fall back to the driver's raw console. */
115
+ on(event: "error", listener: (cause: Error) => void): unknown
116
+ }
117
+
118
+ /** A queue as the transport uses it. */
119
+ export interface BullmqQueue extends BullmqErrorSource {
120
+ /** The Redis backend of the queue. */
121
+ getBackend(): BullmqBackend
122
+ /** Adds the job; an id BullMQ already holds makes the add a no-op. */
123
+ add(name: string, payload: BullmqJobData, options: BullmqJobOptions): Promise<unknown>
124
+ /** The state of the job, `unknown` when Redis does not hold it. */
125
+ getJobState(jobId: string): Promise<string>
126
+ /** Creates or updates the scheduler by its id. */
127
+ upsertJobScheduler(id: string, repeat: { readonly every: number }, template: BullmqJobTemplate): Promise<unknown>
128
+ /** Closes the queue. */
129
+ close(): Promise<void>
130
+ }
131
+
132
+ /** A job as a worker receives it: the fields the transport translates into a QueueDelivery. */
133
+ export interface BullmqJob {
134
+ /** The job id; a tick the scheduler fires can have none. */
135
+ readonly id?: string
136
+ /** Stored data is untrusted: queued jobs can also carry the raw payload written before envelopes. */
137
+ readonly data: unknown
138
+ /** How many deliveries were started before this one. */
139
+ readonly attemptsMade: number
140
+ }
141
+
142
+ /** The durable job data: an enqueued job propagates its id; a scheduler starts a new root per tick. */
143
+ export type BullmqJobData = {
144
+ /** Distinguishes infrastructure envelopes from arbitrary raw payload fields. */
145
+ readonly schema: typeof BULLMQ_JOB_SCHEMA
146
+ } & (
147
+ | { readonly kind: "enqueued"; readonly correlationId: string; readonly payload: object }
148
+ | { readonly kind: "scheduled"; readonly payload: object }
149
+ )
150
+
151
+ /** A worker as the transport uses it. */
152
+ export interface BullmqWorker extends BullmqErrorSource {
153
+ /** Stops the worker; `force` drops the jobs it is still running. */
154
+ close(force?: boolean): Promise<void>
155
+ }
156
+
157
+ /** Builds the BullMQ objects; the module provides it so a spec can double it through the injection token. */
158
+ export interface QueueFactory {
159
+ /** The queue of the name over the shared connection. */
160
+ queue(name: string, options: BullmqConnection): BullmqQueue
161
+ /** A worker that runs the processor for every job of the queue. */
162
+ worker(name: string, processor: (job: BullmqJob) => Promise<void>, options: BullmqWorkerOptions): BullmqWorker
163
+ }
@@ -0,0 +1,17 @@
1
+ import type { EnvSource } from "@modules/platform/config"
2
+ import type { QueueOptions } from "./queue.options"
3
+
4
+ /** What the app's `main.ts` reads for the queues; the connections and the schedulers are the app root's choice. */
5
+ export type QueueConfig = Omit<QueueOptions, "connections" | "schedulers" | "outboxOnly">
6
+
7
+ /** Reads the queue options: the address of the declared Redis is required, its password comes from a mounted secret, the rest are tunables with literal defaults. */
8
+ export const parseQueueConfig = (env: EnvSource): QueueConfig => ({
9
+ redisHost: env.string("QUEUE_REDIS_HOST"),
10
+ redisPort: env.int("QUEUE_REDIS_PORT", 6379),
11
+ redisDb: env.int("QUEUE_REDIS_DB", 0),
12
+ ...(env.has("QUEUE_REDIS_PASSWORD") ? { redisPassword: env.secret("QUEUE_REDIS_PASSWORD") } : {}),
13
+ prefix: env.optional("QUEUE_PREFIX") ?? "queue",
14
+ relayIntervalMs: env.duration("QUEUE_RELAY_INTERVAL", 200),
15
+ relayBatch: env.int("QUEUE_RELAY_BATCH", 50),
16
+ concurrency: env.int("QUEUE_CONCURRENCY", 5),
17
+ })
@@ -0,0 +1,25 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Codes of the queue capability. */
5
+ export enum QueueErrorCode {
6
+ /** Redis did not write the jobs the relay added to its append-only file in time, so their outbox rows stay waiting. */
7
+ NotPersisted = "QUEUE_NOT_PERSISTED",
8
+ /** Redis no longer holds a job the relay added: it restarted before the job reached its append-only file. */
9
+ JobLost = "QUEUE_JOB_LOST",
10
+ /** Stored job data does not satisfy the delivery envelope contract. */
11
+ MalformedMessage = "QUEUE_MALFORMED_MESSAGE",
12
+ /** A producer supplied an identity that is unsafe or unbounded. */
13
+ CorrelationInvalid = "QUEUE_CORRELATION_INVALID",
14
+ }
15
+
16
+ /** How each queue code travels. */
17
+ export const QUEUE_ERROR_KINDS: Record<QueueErrorCode, ErrorKind> = {
18
+ [QueueErrorCode.NotPersisted]: "unavailable",
19
+ [QueueErrorCode.JobLost]: "unavailable",
20
+ [QueueErrorCode.MalformedMessage]: "internal",
21
+ [QueueErrorCode.CorrelationInvalid]: "invalid",
22
+ }
23
+
24
+ /** The one error class of the queue capability. */
25
+ export class QueueError extends DomainError<QueueErrorCode> {}
@@ -0,0 +1,8 @@
1
+ export { QUEUE_ERROR_KINDS } from "./errors/queue.error"
2
+ export { QUEUE_MESSAGES } from "./messages/queue.messages"
3
+ export { parseQueueConfig } from "./queue.config"
4
+ export type { QueueConfig } from "./queue.config"
5
+ export type { QueueDelivery, QueueSchedulerDefinition } from "./queue.contracts"
6
+ export { InjectQueueOutbox, InjectQueueWorkerRegistry, QUEUE_OUTBOX, QUEUE_WORKER_REGISTRY } from "./queue.decorators"
7
+ export { QueueModule } from "./queue.module"
8
+ export type { QueueOutbox, QueueWorkerRegistry } from "./queue.port"
@@ -0,0 +1,21 @@
1
+ import type { MessageBundle } from "@modules/platform/i18n"
2
+
3
+ /** Display text of the queue codes, Vietnamese and English. */
4
+ export const QUEUE_MESSAGES: MessageBundle = {
5
+ vi: {
6
+ "errors.QUEUE_NOT_PERSISTED":
7
+ "Redis chưa ghi các tác vụ vào tệp append-only kịp thời; các dòng outbox vẫn chờ gửi lại.",
8
+ "errors.QUEUE_JOB_LOST":
9
+ "Redis không còn giữ một tác vụ vừa thêm vì đã khởi động lại; dòng outbox của nó sẽ được gửi lại.",
10
+ "errors.QUEUE_MALFORMED_MESSAGE": "Thông điệp hàng đợi đã lưu không hợp lệ.",
11
+ "errors.QUEUE_CORRELATION_INVALID": "Mã tương quan của thông điệp hàng đợi không hợp lệ.",
12
+ },
13
+ en: {
14
+ "errors.QUEUE_NOT_PERSISTED":
15
+ "Redis did not write the queued jobs to its append-only file in time; their outbox rows stay waiting.",
16
+ "errors.QUEUE_JOB_LOST":
17
+ "Redis no longer holds a job it was handed because it restarted; its outbox row is relayed again.",
18
+ "errors.QUEUE_MALFORMED_MESSAGE": "The stored queue message is malformed.",
19
+ "errors.QUEUE_CORRELATION_INVALID": "The queue message correlation id is invalid.",
20
+ },
21
+ }
@@ -0,0 +1,74 @@
1
+ import { Module } from "@nestjs/common"
2
+ import type { DynamicModule } from "@nestjs/common"
3
+ import { Queue, Worker } from "bullmq"
4
+ import type { EntityManager } from "typeorm"
5
+ import {
6
+ QUEUE_FACTORY,
7
+ QUEUE_OUTBOX,
8
+ QUEUE_RELAY_MANAGERS,
9
+ QUEUE_TRANSPORT,
10
+ QUEUE_WORKER_REGISTRY,
11
+ } from "./queue.decorators"
12
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./queue.module-definition"
13
+ import { BullmqQueueTransportClient } from "./bullmq-queue-transport.client"
14
+ import type {
15
+ BullmqConnection,
16
+ BullmqJob,
17
+ BullmqJobData,
18
+ BullmqQueue,
19
+ BullmqWorker,
20
+ BullmqWorkerOptions,
21
+ } from "./queue-transport.port"
22
+ import { QueueOutboxService } from "./queue-outbox.service"
23
+ import { QueueRelayService } from "./queue-relay.service"
24
+ import { QueueWorkerService } from "./queue-worker.service"
25
+
26
+ @Module({})
27
+ /** The queues of a service: the outbox writer, its relay into BullMQ, the workers and the schedulers, registered once per app over the connections that hold an outbox. */
28
+ export class QueueModule extends ConfigurableModuleClass {
29
+ /** Registers the capability once per app that enqueues or processes jobs. */
30
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
31
+ const base = super.register(options)
32
+ const writerProviders = [
33
+ ...(base.providers ?? []),
34
+ QueueOutboxService,
35
+ { provide: QUEUE_OUTBOX, useExisting: QueueOutboxService },
36
+ ]
37
+ if (options.outboxOnly === true) {
38
+ return {
39
+ ...base,
40
+ providers: writerProviders,
41
+ exports: [QUEUE_OUTBOX],
42
+ }
43
+ }
44
+ return {
45
+ ...base,
46
+ providers: [
47
+ ...writerProviders,
48
+ {
49
+ provide: QUEUE_RELAY_MANAGERS,
50
+ useFactory: (...managers: Array<EntityManager>) => managers,
51
+ inject: [...options.connections],
52
+ },
53
+ {
54
+ provide: QUEUE_FACTORY,
55
+ useValue: {
56
+ queue: (name: string, options: BullmqConnection): BullmqQueue =>
57
+ new Queue<object>(name, options),
58
+ worker: (
59
+ name: string,
60
+ processor: (job: BullmqJob) => Promise<void>,
61
+ options: BullmqWorkerOptions,
62
+ ): BullmqWorker => new Worker<BullmqJobData>(name, processor, options),
63
+ },
64
+ },
65
+ BullmqQueueTransportClient,
66
+ { provide: QUEUE_TRANSPORT, useExisting: BullmqQueueTransportClient },
67
+ QueueRelayService,
68
+ QueueWorkerService,
69
+ { provide: QUEUE_WORKER_REGISTRY, useExisting: QueueWorkerService },
70
+ ],
71
+ exports: [QUEUE_OUTBOX, QUEUE_WORKER_REGISTRY, QUEUE_TRANSPORT],
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,38 @@
1
+ import type { InjectionToken } from "@nestjs/common"
2
+ import type { Secret } from "@modules/platform/config"
3
+ import type { QueueSchedulerDefinition } from "./queue.contracts"
4
+
5
+ /** What the queues of one app need: where the declared Redis is, how the relay and the workers run, which connections hold an outbox, and the schedulers it registers. */
6
+ export interface QueueOptions {
7
+ /** The runtime registration owns the relay, workers and schedulers. */
8
+ readonly outboxOnly?: false
9
+ /** The Redis host BullMQ talks to. */
10
+ readonly redisHost: string
11
+ /** The Redis port. */
12
+ readonly redisPort: number
13
+ /** The Redis database that holds this app's queue state. */
14
+ readonly redisDb: number
15
+ /** The Redis password from its sealed secret file; absent for a local Redis without auth. */
16
+ readonly redisPassword?: Secret
17
+ /** What every BullMQ key starts with: the shared default in a deployment, the run prefix of a test world. */
18
+ readonly prefix: string
19
+ /** The pause between two relay passes over an empty outbox. */
20
+ readonly relayIntervalMs: number
21
+ /** The most outbox rows one relay pass hands to BullMQ. */
22
+ readonly relayBatch: number
23
+ /** How many jobs one worker of the app runs at once. */
24
+ readonly concurrency: number
25
+ /** The tokens of the shared entity managers of the connections that hold a queue outbox of the app: the relay reads each of them. */
26
+ readonly connections: ReadonlyArray<InjectionToken>
27
+ /** The job schedulers the app registers at boot; BullMQ upserts them by id, so a pool of replicas fires each one once per interval. */
28
+ readonly schedulers: ReadonlyArray<QueueSchedulerDefinition>
29
+ }
30
+
31
+ /** A one-off producer writes an outbox row and exits; the running app owns delivery. */
32
+ export interface QueueOutboxOnlyOptions {
33
+ /** Registers only the transactional outbox writer, leaving delivery to the running app. */
34
+ readonly outboxOnly: true
35
+ }
36
+
37
+ /** Registers either the running queue capability or only its transactional writer. */
38
+ export type QueueRegistrationOptions = QueueOptions | QueueOutboxOnlyOptions
@@ -17,7 +17,7 @@ export class @@Queue@@Queue {
17
17
  constructor(@InjectQueueOutbox() private readonly outbox: QueueOutbox) {}
18
18
 
19
19
  /** Enqueues one @@queue@@ job in the transaction of the caller: the job exists exactly when the change that needs it commits. */
20
- enqueue@@Queue@@(payload: @@Queue@@Payload, tx: EntityManager): Promise<void> {
21
- return this.outbox.write(tx, @@queueUpper@@_QUEUE, payload)
20
+ enqueue@@Queue@@(payload: @@Queue@@Payload, tx: EntityManager, correlationId: string): Promise<void> {
21
+ return this.outbox.write(tx, @@queueUpper@@_QUEUE, payload, correlationId)
22
22
  }
23
23
  }
@@ -2,6 +2,8 @@ import type { @@Event@@Event } from "@modules/events/@@from@@"
2
2
 
3
3
  /** What applying one delivered @@event@@ event takes. */
4
4
  export interface @@Action@@Request {
5
+ /** The originating request identity retained by the delivery envelope. */
6
+ readonly correlationId: string
5
7
  /** The id of the delivered event; the domain service claims it in the inbox inside its own transaction. */
6
8
  readonly eventId: string
7
9
  /** The delivered event. */
@@ -14,6 +14,10 @@ export class @@Event@@Consumer implements EventConsumer<@@Event@@Event> {
14
14
 
15
15
  /** Delivery is at least once: the domain service the command reaches claims `eventId` in the inbox inside its own transaction. */
16
16
  async handle(delivery: EventDelivery<@@Event@@Event>): Promise<void> {
17
- await this.commandBus.execute(new @@Action@@Command({ request: { eventId: delivery.eventId, event: delivery.event } }))
17
+ await this.commandBus.execute(
18
+ new @@Action@@Command({
19
+ request: { eventId: delivery.eventId, event: delivery.event, correlationId: delivery.correlationId },
20
+ }),
21
+ )
18
22
  }
19
23
  }
@@ -1,9 +1,10 @@
1
1
  import type { Request, Response } from "express"
2
+ import type { CorrelationRequest } from "@modules/platform/logging"
2
3
 
3
4
  /** The GraphQL context every operation runs with: the HTTP request it arrived on and its response. */
4
5
  export interface GraphqlContext {
5
6
  /** The HTTP request: the guards read its headers and the principal they establish on it. */
6
- readonly req: Request
7
+ readonly req: Request & CorrelationRequest
7
8
  /** The HTTP response. */
8
9
  readonly res: Response
9
10
  }
@@ -15,7 +15,7 @@ const requestContext = (
15
15
  headers: Request["headers"],
16
16
  ): GraphQLRequestContextWillSendResponse<GraphqlContext> =>
17
17
  mock<GraphQLRequestContextWillSendResponse<GraphqlContext>>({
18
- contextValue: { req: mock<Request>({ headers }), res: mock<GraphqlContext["res"]>() },
18
+ contextValue: { req: mock<GraphqlContext["req"]>({ headers, correlationId: "request-1" }), res: mock<GraphqlContext["res"]>() },
19
19
  response,
20
20
  })
21
21