@starci/hfs 4.7.32 → 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.
- package/CHANGELOG.md +9 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import { isRecord } from "@modules/platform/primitives"
|
|
3
|
+
import { InjectLogger, isCorrelationId } from "@modules/platform/logging"
|
|
4
|
+
import type { Logger } from "@modules/platform/logging"
|
|
5
|
+
import { InjectIds } from "@modules/platform/ids"
|
|
6
|
+
import type { Ids } from "@modules/platform/ids"
|
|
2
7
|
import { InjectQueueFactory, InjectQueueOptions } from "./queue.decorators"
|
|
3
|
-
import
|
|
8
|
+
import { BULLMQ_JOB_SCHEMA } from "./queue.contracts"
|
|
9
|
+
import type { QueueDelivery, QueueHandler, QueueSchedulerDefinition } from "./queue.contracts"
|
|
10
|
+
import { QueueError, QueueErrorCode } from "./errors/queue.error"
|
|
11
|
+
import { QueueLogEvent } from "./queue.log-events"
|
|
4
12
|
import type { QueueOptions } from "./queue.options"
|
|
5
13
|
import { ATTEMPTS, BACKOFF_MS, KEEP_COMPLETED_SECONDS, jobNameOf } from "./queue.policy"
|
|
6
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
BullmqConnection,
|
|
16
|
+
BullmqJob,
|
|
17
|
+
BullmqQueue,
|
|
18
|
+
BullmqWorker,
|
|
19
|
+
QueueFactory,
|
|
20
|
+
QueueTransport,
|
|
21
|
+
} from "./queue-transport.port"
|
|
7
22
|
|
|
8
23
|
@Injectable()
|
|
9
24
|
/** The QueueTransport port over a Redis the app names in its options; the BullMQ objects come from the injected factory the module composes. */
|
|
@@ -14,16 +29,23 @@ export class BullmqQueueTransportClient implements QueueTransport {
|
|
|
14
29
|
constructor(
|
|
15
30
|
@InjectQueueOptions() private readonly options: QueueOptions,
|
|
16
31
|
@InjectQueueFactory() private readonly factory: QueueFactory,
|
|
32
|
+
@InjectIds() private readonly ids: Ids,
|
|
33
|
+
@InjectLogger() private readonly logger: Logger,
|
|
17
34
|
) {}
|
|
18
35
|
|
|
19
36
|
/** Adds the job with the outbox row id as its BullMQ id, so adding it twice leaves one job. */
|
|
20
|
-
async add(queue: string, jobId: string, payload: object): Promise<void> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
async add(queue: string, jobId: string, payload: object, correlationId: string): Promise<void> {
|
|
38
|
+
if (!isCorrelationId(correlationId)) throw new QueueError({ code: QueueErrorCode.CorrelationInvalid })
|
|
39
|
+
await this.queueOf(queue).add(
|
|
40
|
+
jobNameOf(queue),
|
|
41
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload, correlationId },
|
|
42
|
+
{
|
|
43
|
+
jobId,
|
|
44
|
+
attempts: ATTEMPTS,
|
|
45
|
+
backoff: { type: "exponential", delay: BACKOFF_MS },
|
|
46
|
+
removeOnComplete: { age: KEEP_COMPLETED_SECONDS },
|
|
47
|
+
},
|
|
48
|
+
)
|
|
27
49
|
}
|
|
28
50
|
|
|
29
51
|
/** Creates or updates the scheduler; BullMQ keeps one per id however many replicas call this. */
|
|
@@ -31,25 +53,25 @@ export class BullmqQueueTransportClient implements QueueTransport {
|
|
|
31
53
|
await this.queueOf(scheduler.queue).upsertJobScheduler(
|
|
32
54
|
scheduler.id,
|
|
33
55
|
{ every: scheduler.everyMs },
|
|
34
|
-
{
|
|
56
|
+
{
|
|
57
|
+
name: jobNameOf(scheduler.queue),
|
|
58
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: scheduler.payload ?? {} },
|
|
59
|
+
opts: { attempts: ATTEMPTS },
|
|
60
|
+
},
|
|
35
61
|
)
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
/** Starts a worker of the queue. */
|
|
39
65
|
work(handler: QueueHandler, concurrency: number): Promise<void> {
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(job)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
queue: handler.queue,
|
|
47
|
-
payload: job.data,
|
|
48
|
-
attempt: job.attemptsMade + 1,
|
|
49
|
-
}),
|
|
50
|
-
{ ...this.connection(), concurrency },
|
|
51
|
-
),
|
|
66
|
+
const worker = this.factory.worker(
|
|
67
|
+
handler.queue,
|
|
68
|
+
async (job) => {
|
|
69
|
+
await handler.handle(this.deliveryOf(handler.queue, job))
|
|
70
|
+
},
|
|
71
|
+
{ ...this.connection(), concurrency },
|
|
52
72
|
)
|
|
73
|
+
worker.on("error", (cause) => this.logger.error(QueueLogEvent.WorkerFailed, cause, { queue: handler.queue }))
|
|
74
|
+
this.workers.push(worker)
|
|
53
75
|
return Promise.resolve()
|
|
54
76
|
}
|
|
55
77
|
|
|
@@ -64,6 +86,39 @@ export class BullmqQueueTransportClient implements QueueTransport {
|
|
|
64
86
|
await Promise.all([...this.queues.values()].map((queue) => queue.close()))
|
|
65
87
|
}
|
|
66
88
|
|
|
89
|
+
/** Reads current envelopes and stored raw payloads without changing the identity of a retried job. */
|
|
90
|
+
private deliveryOf(queue: string, job: BullmqJob): QueueDelivery {
|
|
91
|
+
const data = job.data
|
|
92
|
+
if (!isRecord(data)) throw new QueueError({ code: QueueErrorCode.MalformedMessage })
|
|
93
|
+
let payload: object = data
|
|
94
|
+
let correlationId = job.id
|
|
95
|
+
const schema = data.schema
|
|
96
|
+
const schemaPrefix = BULLMQ_JOB_SCHEMA.slice(0, BULLMQ_JOB_SCHEMA.lastIndexOf("@") + 1)
|
|
97
|
+
if (typeof schema === "string" && schema.startsWith(schemaPrefix)) {
|
|
98
|
+
if (
|
|
99
|
+
schema !== BULLMQ_JOB_SCHEMA ||
|
|
100
|
+
(data.kind !== "enqueued" && data.kind !== "scheduled") ||
|
|
101
|
+
!isRecord(data.payload)
|
|
102
|
+
) {
|
|
103
|
+
throw new QueueError({ code: QueueErrorCode.MalformedMessage })
|
|
104
|
+
}
|
|
105
|
+
payload = data.payload
|
|
106
|
+
if (data.kind === "enqueued" || "correlationId" in data) {
|
|
107
|
+
if (!isCorrelationId(data.correlationId)) {
|
|
108
|
+
throw new QueueError({ code: QueueErrorCode.MalformedMessage })
|
|
109
|
+
}
|
|
110
|
+
correlationId = data.correlationId
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id: job.id ?? "",
|
|
115
|
+
queue,
|
|
116
|
+
correlationId: correlationId ?? this.ids.next(),
|
|
117
|
+
payload,
|
|
118
|
+
attempt: job.attemptsMade + 1,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
67
122
|
private connection(): BullmqConnection {
|
|
68
123
|
return {
|
|
69
124
|
prefix: this.options.prefix,
|
|
@@ -80,6 +135,7 @@ export class BullmqQueueTransportClient implements QueueTransport {
|
|
|
80
135
|
const known = this.queues.get(name)
|
|
81
136
|
if (known !== undefined) return known
|
|
82
137
|
const created = this.factory.queue(name, this.connection())
|
|
138
|
+
created.on("error", (cause) => this.logger.error(QueueLogEvent.QueueFailed, cause, { queue: name }))
|
|
83
139
|
this.queues.set(name, created)
|
|
84
140
|
return created
|
|
85
141
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
/** Stored job data does not satisfy the delivery envelope contract. */
|
|
7
|
+
MalformedMessage = "QUEUE_MALFORMED_MESSAGE",
|
|
8
|
+
/** A producer supplied an identity that is unsafe or unbounded. */
|
|
9
|
+
CorrelationInvalid = "QUEUE_CORRELATION_INVALID",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** How each queue code travels. */
|
|
13
|
+
export const QUEUE_ERROR_KINDS: Record<QueueErrorCode, ErrorKind> = {
|
|
14
|
+
[QueueErrorCode.MalformedMessage]: "internal",
|
|
15
|
+
[QueueErrorCode.CorrelationInvalid]: "invalid",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The queue capability's failures, retained by the broker for inspection and retry. */
|
|
19
|
+
export class QueueError extends DomainError<QueueErrorCode> {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MessageBundle } from "@modules/platform/i18n"
|
|
2
|
+
|
|
3
|
+
/** Display text of queue failures in Vietnamese and English. */
|
|
4
|
+
export const QUEUE_MESSAGES: MessageBundle = {
|
|
5
|
+
vi: {
|
|
6
|
+
"errors.QUEUE_MALFORMED_MESSAGE": "Th\u00f4ng \u0111i\u1ec7p h\u00e0ng \u0111\u1ee3i \u0111\u00e3 l\u01b0u kh\u00f4ng h\u1ee3p l\u1ec7.",
|
|
7
|
+
"errors.QUEUE_CORRELATION_INVALID": "M\u00e3 t\u01b0\u01a1ng quan c\u1ee7a th\u00f4ng \u0111i\u1ec7p h\u00e0ng \u0111\u1ee3i kh\u00f4ng h\u1ee3p l\u1ec7.",
|
|
8
|
+
},
|
|
9
|
+
en: {
|
|
10
|
+
"errors.QUEUE_MALFORMED_MESSAGE": "The stored queue message is malformed.",
|
|
11
|
+
"errors.QUEUE_CORRELATION_INVALID": "The queue message correlation id is invalid.",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -10,6 +10,7 @@ export class CreateQueueOutbox@@epochMs13@@ implements MigrationInterface {
|
|
|
10
10
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
11
11
|
queue varchar(200) NOT NULL,
|
|
12
12
|
payload jsonb NOT NULL,
|
|
13
|
+
correlation_id varchar(128) NOT NULL DEFAULT pg_catalog.gen_random_uuid()::text,
|
|
13
14
|
created_at timestamptz NOT NULL,
|
|
14
15
|
sent_at timestamptz
|
|
15
16
|
)`)
|
|
@@ -3,6 +3,10 @@ import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"
|
|
|
3
3
|
@Entity("queue_outbox")
|
|
4
4
|
/** One job waiting to start: written in the transaction of the change that needs it, handed to BullMQ by the relay. */
|
|
5
5
|
export class QueueOutboxEntity {
|
|
6
|
+
/** The upstream correlation id persisted with the job. */
|
|
7
|
+
@Column({ name: "correlation_id", type: "varchar", length: 128, default: () => "pg_catalog.gen_random_uuid()::text" })
|
|
8
|
+
correlationId!: string
|
|
9
|
+
|
|
6
10
|
/** The row id; it becomes the BullMQ job id. */
|
|
7
11
|
@PrimaryGeneratedColumn("uuid", { name: "id" })
|
|
8
12
|
id!: string
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** One row the relay reads from the queue outbox. */
|
|
2
2
|
export interface QueueRow {
|
|
3
|
+
/** The correlation id recorded by the producer's transaction. */
|
|
4
|
+
readonly correlationId: string
|
|
3
5
|
/** The row id: it is the BullMQ job id, so a repeated relay pass adds nothing. */
|
|
4
6
|
readonly id: string
|
|
5
7
|
/** The queue the job goes to. */
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { sql } from "@modules/platform/database"
|
|
2
2
|
|
|
3
|
-
/** Writes one job in the outbox ($1 queue, $2 payload, $3 instant). */
|
|
4
|
-
export const INSERT_QUEUE_ROW = sql`INSERT INTO queue_outbox (queue, payload, created_at)
|
|
5
|
-
VALUES ($1, $2, $3)`
|
|
3
|
+
/** Writes one job in the outbox ($1 queue, $2 payload, $3 instant, $4 correlation id). */
|
|
4
|
+
export const INSERT_QUEUE_ROW = sql`INSERT INTO queue_outbox (queue, payload, created_at, correlation_id)
|
|
5
|
+
VALUES ($1, $2, $3, $4)`
|
|
6
6
|
|
|
7
7
|
/** Locks and reads the oldest rows that wait ($1 batch size); a second relay skips the rows this one holds. */
|
|
8
|
-
export const SELECT_WAITING_ROWS = sql`SELECT id, queue, payload FROM queue_outbox
|
|
8
|
+
export const SELECT_WAITING_ROWS = sql`SELECT id, queue, payload, correlation_id AS "correlationId" FROM queue_outbox
|
|
9
9
|
WHERE sent_at IS NULL ORDER BY created_at, id LIMIT $1 FOR UPDATE SKIP LOCKED`
|
|
10
10
|
|
|
11
11
|
/** Marks the rows as handed to BullMQ ($1 ids, $2 instant). */
|
|
@@ -3,6 +3,7 @@ import { FakeClock, fakeTransaction, mockEntityManager } from "@starci/jest-pres
|
|
|
3
3
|
import { CLOCK } from "@modules/platform/clock"
|
|
4
4
|
import { QueueOutboxService } from "./queue-outbox.service"
|
|
5
5
|
import { INSERT_QUEUE_ROW } from "./persistence/queue.sql"
|
|
6
|
+
import { QueueErrorCode } from "./errors/queue.error"
|
|
6
7
|
|
|
7
8
|
const AT = "2026-02-03T04:05:06.000Z"
|
|
8
9
|
|
|
@@ -19,12 +20,13 @@ describe("QueueOutboxService", () => {
|
|
|
19
20
|
const { outbox } = await build()
|
|
20
21
|
const tx = fakeTransaction(mockEntityManager({ query: [INSERT_QUEUE_ROW, []] }))
|
|
21
22
|
|
|
22
|
-
await tx.em.transaction((manager) => outbox.write(manager, "mail", { id: "m-1" }))
|
|
23
|
+
await tx.em.transaction((manager) => outbox.write(manager, "mail", { id: "m-1" }, "request-1"))
|
|
23
24
|
|
|
24
25
|
expect(tx.em.query).toHaveBeenCalledWith(INSERT_QUEUE_ROW, [
|
|
25
26
|
"mail",
|
|
26
27
|
JSON.stringify({ id: "m-1" }),
|
|
27
28
|
new Date(AT),
|
|
29
|
+
"request-1",
|
|
28
30
|
])
|
|
29
31
|
expect(tx.commits).toBe(1)
|
|
30
32
|
})
|
|
@@ -36,7 +38,7 @@ describe("QueueOutboxService", () => {
|
|
|
36
38
|
|
|
37
39
|
await expect(
|
|
38
40
|
tx.em.transaction(async (manager) => {
|
|
39
|
-
await outbox.write(manager, "mail", { id: "m-1" })
|
|
41
|
+
await outbox.write(manager, "mail", { id: "m-1" }, "request-1")
|
|
40
42
|
return Promise.reject(failure)
|
|
41
43
|
}),
|
|
42
44
|
).rejects.toBe(failure)
|
|
@@ -44,5 +46,15 @@ describe("QueueOutboxService", () => {
|
|
|
44
46
|
expect(tx.rollbacks).toBe(1)
|
|
45
47
|
expect(tx.committedWrites).toEqual([])
|
|
46
48
|
})
|
|
49
|
+
|
|
50
|
+
it("rejects an unsafe correlation id before writing any SQL", async () => {
|
|
51
|
+
const { outbox } = await build()
|
|
52
|
+
const tx = fakeTransaction(mockEntityManager())
|
|
53
|
+
await expect(
|
|
54
|
+
tx.em.transaction((manager) => outbox.write(manager, "mail", {}, "bad\nid")),
|
|
55
|
+
).rejects.toMatchObject({ code: QueueErrorCode.CorrelationInvalid })
|
|
56
|
+
expect(tx.em.query).not.toHaveBeenCalled()
|
|
57
|
+
expect(tx.rollbacks).toBe(1)
|
|
58
|
+
})
|
|
47
59
|
})
|
|
48
60
|
})
|
|
@@ -2,7 +2,9 @@ import { Injectable } from "@nestjs/common"
|
|
|
2
2
|
import type { EntityManager } from "typeorm"
|
|
3
3
|
import { InjectClock } from "@modules/platform/clock"
|
|
4
4
|
import type { Clock } from "@modules/platform/clock"
|
|
5
|
+
import { isCorrelationId } from "@modules/platform/logging"
|
|
5
6
|
import type { QueueOutbox } from "./queue.port"
|
|
7
|
+
import { QueueError, QueueErrorCode } from "./errors/queue.error"
|
|
6
8
|
import { INSERT_QUEUE_ROW } from "./persistence/queue.sql"
|
|
7
9
|
|
|
8
10
|
@Injectable()
|
|
@@ -11,7 +13,8 @@ export class QueueOutboxService implements QueueOutbox {
|
|
|
11
13
|
constructor(@InjectClock() private readonly clock: Clock) {}
|
|
12
14
|
|
|
13
15
|
/** Writes the job in the outbox of the transaction `tx`; the relay adds it to BullMQ after the commit. */
|
|
14
|
-
async write(tx: EntityManager, queue: string, payload: object): Promise<void> {
|
|
15
|
-
|
|
16
|
+
async write(tx: EntityManager, queue: string, payload: object, correlationId: string): Promise<void> {
|
|
17
|
+
if (!isCorrelationId(correlationId)) throw new QueueError({ code: QueueErrorCode.CorrelationInvalid })
|
|
18
|
+
await tx.query(INSERT_QUEUE_ROW, [queue, JSON.stringify(payload), this.clock.now(), correlationId])
|
|
16
19
|
}
|
|
17
20
|
}
|
|
@@ -25,7 +25,7 @@ const options: QueueOptions = {
|
|
|
25
25
|
schedulers: [],
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const row = (id: string) => ({ id, queue: "mail", payload: { id: `m-${id}` } })
|
|
28
|
+
const row = (id: string) => ({ id, queue: "mail", payload: { id: `m-${id}` }, correlationId: `request-${id}` })
|
|
29
29
|
|
|
30
30
|
const build = async (manager: MockEntityManager) => {
|
|
31
31
|
const transport = mock<QueueTransport>()
|
|
@@ -43,87 +43,85 @@ const build = async (manager: MockEntityManager) => {
|
|
|
43
43
|
return { relay: moduleRef.get(QueueRelayService), transport, logger }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
describe("QueueRelayService", () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
46
|
+
describe("QueueRelayService relay", () => {
|
|
47
|
+
it("adds the waiting rows to BullMQ with the row id as the job id and marks them sent in the same transaction", 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(tx.em.query).toHaveBeenNthCalledWith(2, MARK_ROWS_SENT, [["1", "2"], new Date(AT)])
|
|
64
|
+
expect(tx.commits).toBe(1)
|
|
65
|
+
})
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
it("adds nothing and marks nothing when no row waits", async () => {
|
|
68
|
+
const tx = fakeTransaction(mockEntityManager({ query: [SELECT_WAITING_ROWS, []] }))
|
|
69
|
+
const { relay, transport } = await build(tx.em)
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
await expect(relay.relay()).resolves.toBe(0)
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
expect(transport.add).not.toHaveBeenCalled()
|
|
74
|
+
expect(tx.em.query).toHaveBeenCalledTimes(1)
|
|
75
|
+
})
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
it("leaves the rows unmarked when BullMQ refuses, so the next pass adds them again", async () => {
|
|
78
|
+
const tx = fakeTransaction(mockEntityManager({ query: [SELECT_WAITING_ROWS, [row("1")]] }))
|
|
79
|
+
const { relay, transport } = await build(tx.em)
|
|
80
|
+
const failure = new Error("redis down")
|
|
81
|
+
transport.add.mockRejectedValue(failure)
|
|
83
82
|
|
|
84
|
-
|
|
83
|
+
await expect(relay.relay()).rejects.toBe(failure)
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
85
|
+
expect(tx.rollbacks).toBe(1)
|
|
86
|
+
expect(tx.em.query).toHaveBeenCalledTimes(1)
|
|
89
87
|
})
|
|
88
|
+
})
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
relay.onApplicationBootstrap()
|
|
121
|
-
await secondWait
|
|
122
|
-
await shutdown
|
|
123
|
-
|
|
124
|
-
expect(transport.add).toHaveBeenCalledTimes(1)
|
|
125
|
-
expect(transport.wait).toHaveBeenCalledWith(100)
|
|
126
|
-
expect(logger.error).toHaveBeenCalledWith(OutboxLogEvent.RelayFailed, failure, { outbox: "queue" })
|
|
90
|
+
describe("QueueRelayService the loop", () => {
|
|
91
|
+
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 () => {
|
|
92
|
+
const tx = fakeTransaction(
|
|
93
|
+
mockEntityManager({
|
|
94
|
+
query: [
|
|
95
|
+
[SELECT_WAITING_ROWS, [row("1")]],
|
|
96
|
+
[MARK_ROWS_SENT, []],
|
|
97
|
+
[SELECT_WAITING_ROWS, []],
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
const { relay, transport, logger } = await build(tx.em)
|
|
102
|
+
const failure = new Error("outbox unreadable")
|
|
103
|
+
let waits = 0
|
|
104
|
+
let shutdown: Promise<void> = Promise.resolve()
|
|
105
|
+
let reachedSecondWait: () => void = () => undefined
|
|
106
|
+
const secondWait = new Promise<void>((resolve) => {
|
|
107
|
+
reachedSecondWait = resolve
|
|
108
|
+
})
|
|
109
|
+
transport.wait.mockImplementation(() => {
|
|
110
|
+
waits += 1
|
|
111
|
+
if (waits === 1) tx.em.query.mockRejectedValueOnce(failure)
|
|
112
|
+
else {
|
|
113
|
+
shutdown = relay.onApplicationShutdown()
|
|
114
|
+
reachedSecondWait()
|
|
115
|
+
}
|
|
116
|
+
return Promise.resolve()
|
|
127
117
|
})
|
|
118
|
+
|
|
119
|
+
relay.onApplicationBootstrap()
|
|
120
|
+
await secondWait
|
|
121
|
+
await shutdown
|
|
122
|
+
|
|
123
|
+
expect(transport.add).toHaveBeenCalledTimes(1)
|
|
124
|
+
expect(transport.wait).toHaveBeenCalledWith(100)
|
|
125
|
+
expect(logger.error).toHaveBeenCalledWith(OutboxLogEvent.RelayFailed, failure, { outbox: "queue" })
|
|
128
126
|
})
|
|
129
127
|
})
|
|
@@ -36,7 +36,7 @@ export class QueueRelayService extends OutboxRelayPolicy<QueueRow> {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
protected deliver(rows: ReadonlyArray<QueueRow>): Promise<void> {
|
|
39
|
-
return eachInOrder(rows, (row) => this.transport.add(row.queue, row.id, row.payload))
|
|
39
|
+
return eachInOrder(rows, (row) => this.transport.add(row.queue, row.id, row.payload, row.correlationId))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
protected async mark(tx: EntityManager, rows: ReadonlyArray<QueueRow>): Promise<void> {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { QueueHandler, QueueSchedulerDefinition } from "./queue.contracts"
|
|
1
|
+
import type { BULLMQ_JOB_SCHEMA, QueueHandler, QueueSchedulerDefinition } from "./queue.contracts"
|
|
2
2
|
|
|
3
3
|
/** BullMQ as the queue capability sees it: the one port the BullMQ client implements. */
|
|
4
4
|
export interface QueueTransport {
|
|
5
5
|
/** Adds the job to its queue with `jobId`; adding an id BullMQ already holds changes nothing, so a repeated relay pass is harmless. */
|
|
6
|
-
add(queue: string, jobId: string, payload: object): Promise<void>
|
|
6
|
+
add(queue: string, jobId: string, payload: object, correlationId: string): Promise<void>
|
|
7
7
|
/** Creates or updates a job scheduler by its id. */
|
|
8
8
|
upsertScheduler(scheduler: QueueSchedulerDefinition): Promise<void>
|
|
9
9
|
/** Starts a worker of the queue the handler names that runs it for every job, `concurrency` at a time. */
|
|
@@ -77,15 +77,21 @@ export interface BullmqJobTemplate {
|
|
|
77
77
|
/** The job name, which is the queue name. */
|
|
78
78
|
readonly name: string
|
|
79
79
|
/** The payload of every tick. */
|
|
80
|
-
readonly data:
|
|
80
|
+
readonly data: BullmqJobData
|
|
81
81
|
/** The options of every tick. */
|
|
82
82
|
readonly opts: BullmqSchedulerJobOptions
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** A BullMQ driver whose infrastructure failures must use the application logger. */
|
|
86
|
+
export interface BullmqErrorSource {
|
|
87
|
+
/** Subscribes before asynchronous driver errors can fall back to the driver's raw console. */
|
|
88
|
+
on(event: "error", listener: (cause: Error) => void): unknown
|
|
89
|
+
}
|
|
90
|
+
|
|
85
91
|
/** A queue as the transport uses it. */
|
|
86
|
-
export interface BullmqQueue {
|
|
92
|
+
export interface BullmqQueue extends BullmqErrorSource {
|
|
87
93
|
/** Adds the job; an id BullMQ already holds makes the add a no-op. */
|
|
88
|
-
add(name: string, payload:
|
|
94
|
+
add(name: string, payload: BullmqJobData, options: BullmqJobOptions): Promise<unknown>
|
|
89
95
|
/** Creates or updates the scheduler by its id. */
|
|
90
96
|
upsertJobScheduler(id: string, repeat: { readonly every: number }, template: BullmqJobTemplate): Promise<unknown>
|
|
91
97
|
/** Closes the queue. */
|
|
@@ -96,14 +102,23 @@ export interface BullmqQueue {
|
|
|
96
102
|
export interface BullmqJob {
|
|
97
103
|
/** The job id; a tick the scheduler fires can have none. */
|
|
98
104
|
readonly id?: string
|
|
99
|
-
/**
|
|
100
|
-
readonly data:
|
|
105
|
+
/** Stored data is untrusted: queued jobs can also carry the raw payload written before envelopes. */
|
|
106
|
+
readonly data: unknown
|
|
101
107
|
/** How many deliveries were started before this one. */
|
|
102
108
|
readonly attemptsMade: number
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
/** The durable job data: an enqueued job propagates its id; a scheduler starts a new root per tick. */
|
|
112
|
+
export type BullmqJobData = {
|
|
113
|
+
/** Distinguishes infrastructure envelopes from arbitrary raw payload fields. */
|
|
114
|
+
readonly schema: typeof BULLMQ_JOB_SCHEMA
|
|
115
|
+
} & (
|
|
116
|
+
| { readonly kind: "enqueued"; readonly correlationId: string; readonly payload: object }
|
|
117
|
+
| { readonly kind: "scheduled"; readonly payload: object }
|
|
118
|
+
)
|
|
119
|
+
|
|
105
120
|
/** A worker as the transport uses it. */
|
|
106
|
-
export interface BullmqWorker {
|
|
121
|
+
export interface BullmqWorker extends BullmqErrorSource {
|
|
107
122
|
/** Stops the worker; `force` drops the jobs it is still running. */
|
|
108
123
|
close(force?: boolean): Promise<void>
|
|
109
124
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Test } from "@nestjs/testing"
|
|
2
2
|
import { mock } from "@starci/jest-preset"
|
|
3
|
-
import { LOGGER } from "@modules/platform/logging"
|
|
4
|
-
import type { Logger } from "@modules/platform/logging"
|
|
3
|
+
import { CORRELATION_RUNNER, LOGGER } from "@modules/platform/logging"
|
|
4
|
+
import type { CorrelationRunner, Logger } from "@modules/platform/logging"
|
|
5
5
|
import type { QueueHandler } from "./queue.contracts"
|
|
6
6
|
import { QUEUE_OPTIONS, QUEUE_TRANSPORT } from "./queue.decorators"
|
|
7
7
|
import { QueueLogEvent } from "./queue.log-events"
|
|
@@ -27,15 +27,18 @@ const build = async () => {
|
|
|
27
27
|
const transport = mock<QueueTransport>()
|
|
28
28
|
transport.upsertScheduler.mockResolvedValue(undefined)
|
|
29
29
|
const logger = mock<Logger>()
|
|
30
|
+
const correlation = mock<CorrelationRunner>()
|
|
31
|
+
correlation.run.mockImplementation((_id, work) => work())
|
|
30
32
|
const moduleRef = await Test.createTestingModule({
|
|
31
33
|
providers: [
|
|
32
34
|
QueueWorkerService,
|
|
33
35
|
{ provide: QUEUE_OPTIONS, useValue: options },
|
|
34
36
|
{ provide: QUEUE_TRANSPORT, useValue: transport },
|
|
35
37
|
{ provide: LOGGER, useValue: logger },
|
|
38
|
+
{ provide: CORRELATION_RUNNER, useValue: correlation },
|
|
36
39
|
],
|
|
37
40
|
}).compile()
|
|
38
|
-
return { workers: moduleRef.get(QueueWorkerService), transport, logger }
|
|
41
|
+
return { workers: moduleRef.get(QueueWorkerService), transport, logger, correlation }
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
describe("QueueWorkerService", () => {
|
|
@@ -49,22 +52,40 @@ describe("QueueWorkerService", () => {
|
|
|
49
52
|
|
|
50
53
|
await workers.onApplicationBootstrap()
|
|
51
54
|
|
|
52
|
-
expect(transport.work).toHaveBeenCalledWith(
|
|
53
|
-
expect(transport.work).toHaveBeenCalledWith(
|
|
55
|
+
expect(transport.work).toHaveBeenCalledWith(expect.objectContaining({ queue: "mail" }), 3)
|
|
56
|
+
expect(transport.work).toHaveBeenCalledWith(expect.objectContaining({ queue: "sweep" }), 3)
|
|
54
57
|
expect(transport.upsertScheduler).toHaveBeenCalledWith(options.schedulers[0])
|
|
55
58
|
})
|
|
56
59
|
|
|
57
60
|
it("keeps the last handler registered for a queue", async () => {
|
|
58
61
|
const { workers, transport } = await build()
|
|
59
|
-
const first =
|
|
60
|
-
const second =
|
|
62
|
+
const first = mock<QueueHandler>({ queue: "mail" })
|
|
63
|
+
const second = mock<QueueHandler>({ queue: "mail" })
|
|
64
|
+
second.handle.mockResolvedValue(undefined)
|
|
61
65
|
workers.add(first)
|
|
62
66
|
workers.add(second)
|
|
63
67
|
|
|
64
68
|
await workers.onApplicationBootstrap()
|
|
65
69
|
|
|
66
70
|
expect(transport.work).toHaveBeenCalledTimes(1)
|
|
67
|
-
|
|
71
|
+
const delivery = { id: "job-1", queue: "mail", payload: {}, attempt: 1, correlationId: "request-1" }
|
|
72
|
+
await transport.work.mock.calls[0]?.[0].handle(delivery)
|
|
73
|
+
expect(second.handle).toHaveBeenCalledWith(delivery)
|
|
74
|
+
expect(first.handle).not.toHaveBeenCalled()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("runs each delivery within its explicitly carried logging context", async () => {
|
|
78
|
+
const { workers, transport, correlation } = await build()
|
|
79
|
+
const handler = mock<QueueHandler>({ queue: "mail" })
|
|
80
|
+
handler.handle.mockResolvedValue(undefined)
|
|
81
|
+
workers.add(handler)
|
|
82
|
+
await workers.onApplicationBootstrap()
|
|
83
|
+
const delivery = { id: "job-1", queue: "mail", payload: {}, attempt: 2, correlationId: "request-1" }
|
|
84
|
+
|
|
85
|
+
await transport.work.mock.calls[0]?.[0].handle(delivery)
|
|
86
|
+
|
|
87
|
+
expect(correlation.run).toHaveBeenCalledWith("request-1", expect.any(Function))
|
|
88
|
+
expect(handler.handle).toHaveBeenCalledWith(delivery)
|
|
68
89
|
})
|
|
69
90
|
|
|
70
91
|
it("logs a scheduler that cannot be registered and still starts the app", async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Injectable } from "@nestjs/common"
|
|
2
2
|
import type { OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common"
|
|
3
|
-
import { InjectLogger } from "@modules/platform/logging"
|
|
4
|
-
import type { Logger } from "@modules/platform/logging"
|
|
3
|
+
import { InjectCorrelationRunner, InjectLogger } from "@modules/platform/logging"
|
|
4
|
+
import type { CorrelationRunner, Logger } from "@modules/platform/logging"
|
|
5
5
|
import { eachInOrder } from "@modules/platform/primitives"
|
|
6
6
|
import type { QueueHandler } from "./queue.contracts"
|
|
7
7
|
import { InjectQueueOptions, InjectQueueTransport } from "./queue.decorators"
|
|
@@ -22,11 +22,15 @@ export class QueueWorkerService implements QueueWorkerRegistry, OnApplicationBoo
|
|
|
22
22
|
@InjectQueueOptions() private readonly options: QueueOptions,
|
|
23
23
|
@InjectQueueTransport() private readonly transport: QueueTransport,
|
|
24
24
|
@InjectLogger() private readonly logger: Logger,
|
|
25
|
+
@InjectCorrelationRunner() private readonly correlation: CorrelationRunner,
|
|
25
26
|
) {}
|
|
26
27
|
|
|
27
28
|
/** Registers the handler under the queue it names; a second handler for the same queue replaces the first. */
|
|
28
29
|
add(handler: QueueHandler): void {
|
|
29
|
-
this.handlers.set(handler.queue,
|
|
30
|
+
this.handlers.set(handler.queue, {
|
|
31
|
+
queue: handler.queue,
|
|
32
|
+
handle: (delivery) => this.correlation.run(delivery.correlationId, () => handler.handle(delivery)),
|
|
33
|
+
})
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/** Starts one worker per registered queue, then registers the schedulers of the options. */
|