@starci/hfs 4.7.32 → 4.7.34
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 +10 -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 +56 -16
- 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,3 +1,6 @@
|
|
|
1
|
+
/** The owned wire protocol marker of current BullMQ job envelopes. */
|
|
2
|
+
export const BULLMQ_JOB_SCHEMA = "starci/queue@1"
|
|
3
|
+
|
|
1
4
|
/** A recurring task: BullMQ upserts it by `id`, so every replica of an app may register it and the deployment still fires it once per interval. */
|
|
2
5
|
export interface QueueSchedulerDefinition {
|
|
3
6
|
/** The queue the ticks go to; its processor handles every tick like any other job. */
|
|
@@ -12,6 +15,8 @@ export interface QueueSchedulerDefinition {
|
|
|
12
15
|
|
|
13
16
|
/** One job as a handler receives it. */
|
|
14
17
|
export interface QueueDelivery {
|
|
18
|
+
/** The upstream correlation id, carried explicitly across the queue. */
|
|
19
|
+
readonly correlationId: string
|
|
15
20
|
/** The BullMQ job id: the outbox row id of an enqueued job, the tick id of a scheduled one. */
|
|
16
21
|
readonly id: string
|
|
17
22
|
/** The queue. */
|
|
@@ -5,3 +5,5 @@ export { InjectQueueOutbox, InjectQueueWorkerRegistry, QUEUE_OUTBOX, QUEUE_WORKE
|
|
|
5
5
|
export { QueueModule } from "./queue.module"
|
|
6
6
|
export type { QueueOutbox, QueueWorkerRegistry } from "./queue.port"
|
|
7
7
|
export { queueEntities, queueMigrations } from "./persistence/connection"
|
|
8
|
+
export { QUEUE_ERROR_KINDS } from "./errors/queue.error"
|
|
9
|
+
export { QUEUE_MESSAGES } from "./messages/queue.messages"
|
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
export enum QueueLogEvent {
|
|
3
3
|
/** A scheduler could not be registered at boot. */
|
|
4
4
|
SchedulerFailed = "queue.scheduler.failed",
|
|
5
|
+
/** The producer's BullMQ driver reported an infrastructure error. */
|
|
6
|
+
QueueFailed = "queue.driver.failed",
|
|
7
|
+
/** A worker's BullMQ driver reported an infrastructure error. */
|
|
8
|
+
WorkerFailed = "queue.worker.failed",
|
|
5
9
|
}
|
|
@@ -14,6 +14,7 @@ import { BullmqQueueTransportClient } from "./bullmq-queue-transport.client"
|
|
|
14
14
|
import type {
|
|
15
15
|
BullmqConnection,
|
|
16
16
|
BullmqJob,
|
|
17
|
+
BullmqJobData,
|
|
17
18
|
BullmqQueue,
|
|
18
19
|
BullmqWorker,
|
|
19
20
|
BullmqWorkerOptions,
|
|
@@ -56,7 +57,7 @@ export class QueueModule extends ConfigurableModuleClass {
|
|
|
56
57
|
name: string,
|
|
57
58
|
processor: (job: BullmqJob) => Promise<void>,
|
|
58
59
|
options: BullmqWorkerOptions,
|
|
59
|
-
): BullmqWorker => new Worker<
|
|
60
|
+
): BullmqWorker => new Worker<BullmqJobData>(name, processor, options),
|
|
60
61
|
},
|
|
61
62
|
},
|
|
62
63
|
BullmqQueueTransportClient,
|
|
@@ -4,7 +4,7 @@ import type { QueueHandler } from "./queue.contracts"
|
|
|
4
4
|
/** The queue outbox: a typed producer writes a job row in the transaction of the caller, the relay hands it to BullMQ after commit. */
|
|
5
5
|
export interface QueueOutbox {
|
|
6
6
|
/** Writes the job in the outbox of the transaction `tx`, so it exists exactly when the change that needs it commits. */
|
|
7
|
-
write(tx: EntityManager, queue: string, payload: object): Promise<void>
|
|
7
|
+
write(tx: EntityManager, queue: string, payload: object, correlationId: string): Promise<void>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** Where a processor registers the handler of its queue; the workers start when the app has registered every handler. */
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { fakeIds, mock } from "@starci/jest-preset"
|
|
3
|
+
import { Secret } from "@modules/platform/config"
|
|
4
|
+
import { LOGGER } from "@modules/platform/logging"
|
|
5
|
+
import type { Logger } from "@modules/platform/logging"
|
|
6
|
+
import { IDS } from "@modules/platform/ids"
|
|
7
|
+
import { QueueError, QueueErrorCode } from "./errors/queue.error"
|
|
8
|
+
import { BullmqQueueTransportClient } from "./bullmq-queue-transport.client"
|
|
9
|
+
import { BULLMQ_JOB_SCHEMA } from "./queue.contracts"
|
|
10
|
+
import type { QueueHandler } from "./queue.contracts"
|
|
11
|
+
import { QUEUE_FACTORY, QUEUE_OPTIONS } from "./queue.decorators"
|
|
12
|
+
import { QueueLogEvent } from "./queue.log-events"
|
|
13
|
+
import type { QueueOptions } from "./queue.options"
|
|
14
|
+
import { ATTEMPTS, BACKOFF_MS, KEEP_COMPLETED_SECONDS } from "./queue.policy"
|
|
15
|
+
import type { BullmqCommands, BullmqJob, BullmqQueue, BullmqWorker, QueueFactory } from "./queue-transport.port"
|
|
16
|
+
|
|
17
|
+
const MINTED_ID = "00000000-0000-4000-8000-000000000001"
|
|
18
|
+
|
|
19
|
+
const options: QueueOptions = {
|
|
20
|
+
redisHost: "redis.test",
|
|
21
|
+
redisPort: 6380,
|
|
22
|
+
redisDb: 7,
|
|
23
|
+
prefix: "spec.",
|
|
24
|
+
relayIntervalMs: 100,
|
|
25
|
+
relayBatch: 50,
|
|
26
|
+
concurrency: 4,
|
|
27
|
+
connections: [],
|
|
28
|
+
schedulers: [],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const build = async (redisPassword?: Secret) => {
|
|
32
|
+
const commands = mock<BullmqCommands>()
|
|
33
|
+
const queue = mock<BullmqQueue>()
|
|
34
|
+
queue.getBackend.mockReturnValue({ client: Promise.resolve(commands) })
|
|
35
|
+
const worker = mock<BullmqWorker>()
|
|
36
|
+
const factory = mock<QueueFactory>()
|
|
37
|
+
const ids = fakeIds()
|
|
38
|
+
const logger = mock<Logger>()
|
|
39
|
+
factory.queue.mockReturnValue(queue)
|
|
40
|
+
factory.worker.mockReturnValue(worker)
|
|
41
|
+
const moduleRef = await Test.createTestingModule({
|
|
42
|
+
providers: [
|
|
43
|
+
BullmqQueueTransportClient,
|
|
44
|
+
{ provide: QUEUE_OPTIONS, useValue: { ...options, redisPassword } },
|
|
45
|
+
{ provide: QUEUE_FACTORY, useValue: factory },
|
|
46
|
+
{ provide: IDS, useValue: ids },
|
|
47
|
+
{ provide: LOGGER, useValue: logger },
|
|
48
|
+
],
|
|
49
|
+
}).compile()
|
|
50
|
+
return { client: moduleRef.get(BullmqQueueTransportClient), queue, worker, factory, logger, ids, commands }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("BullmqQueueTransportClient connection", () => {
|
|
54
|
+
it("authenticates with the password of the declared Redis read from its sealed secret", async () => {
|
|
55
|
+
const { client, factory } = await build(new Secret("redis-secret"))
|
|
56
|
+
|
|
57
|
+
await client.add("orders", "outbox-1", {}, "request-1")
|
|
58
|
+
|
|
59
|
+
expect(factory.queue).toHaveBeenCalledWith("orders", {
|
|
60
|
+
prefix: "spec.",
|
|
61
|
+
connection: { host: "redis.test", port: 6380, db: 7, password: "redis-secret", maxRetriesPerRequest: null },
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("BullmqQueueTransportClient jobs and schedulers", () => {
|
|
67
|
+
it("adds idempotent jobs with the queue retry and retention policy", async () => {
|
|
68
|
+
const { client, queue, factory } = await build()
|
|
69
|
+
const payload = { orderId: "order-1" }
|
|
70
|
+
|
|
71
|
+
await client.add("orders", "outbox-1", payload, "request-1")
|
|
72
|
+
await client.add("orders", "outbox-2", payload, "request-2")
|
|
73
|
+
|
|
74
|
+
expect(factory.queue).toHaveBeenCalledTimes(1)
|
|
75
|
+
expect(factory.queue).toHaveBeenCalledWith("orders", {
|
|
76
|
+
prefix: "spec.",
|
|
77
|
+
connection: { host: "redis.test", port: 6380, db: 7, maxRetriesPerRequest: null },
|
|
78
|
+
})
|
|
79
|
+
expect(queue.add).toHaveBeenNthCalledWith(
|
|
80
|
+
1,
|
|
81
|
+
"orders",
|
|
82
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload, correlationId: "request-1" },
|
|
83
|
+
{
|
|
84
|
+
jobId: "outbox-1",
|
|
85
|
+
attempts: ATTEMPTS,
|
|
86
|
+
backoff: { type: "exponential", delay: BACKOFF_MS },
|
|
87
|
+
removeOnComplete: { age: KEEP_COMPLETED_SECONDS },
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("keeps the job id of an outbox row whatever correlation id it carries", async () => {
|
|
93
|
+
const { client, queue } = await build()
|
|
94
|
+
|
|
95
|
+
await client.add("orders", "outbox-1", {}, "request-1")
|
|
96
|
+
await client.add("orders", "outbox-1", {}, "request-2")
|
|
97
|
+
|
|
98
|
+
expect(queue.add.mock.calls.map(([, , options]) => options.jobId)).toEqual(["outbox-1", "outbox-1"])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("rejects unsafe correlation before writing a new job", async () => {
|
|
102
|
+
const { client, factory } = await build()
|
|
103
|
+
|
|
104
|
+
await expect(client.add("orders", "outbox-1", {}, "unsafe\nrequest")).rejects.toMatchObject({
|
|
105
|
+
code: QueueErrorCode.CorrelationInvalid,
|
|
106
|
+
})
|
|
107
|
+
expect(factory.queue).not.toHaveBeenCalled()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("upserts schedulers with their payload or an empty payload", async () => {
|
|
111
|
+
const { client, queue } = await build()
|
|
112
|
+
|
|
113
|
+
await client.upsertScheduler({ queue: "billing", id: "settlement", everyMs: 60_000, payload: { shard: 2 } })
|
|
114
|
+
await client.upsertScheduler({ queue: "billing", id: "cleanup", everyMs: 120_000 })
|
|
115
|
+
|
|
116
|
+
expect(queue.upsertJobScheduler).toHaveBeenNthCalledWith(
|
|
117
|
+
1,
|
|
118
|
+
"settlement",
|
|
119
|
+
{ every: 60_000 },
|
|
120
|
+
{
|
|
121
|
+
name: "billing",
|
|
122
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: { shard: 2 } },
|
|
123
|
+
opts: { attempts: ATTEMPTS },
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
expect(queue.upsertJobScheduler).toHaveBeenNthCalledWith(
|
|
127
|
+
2,
|
|
128
|
+
"cleanup",
|
|
129
|
+
{ every: 120_000 },
|
|
130
|
+
{
|
|
131
|
+
name: "billing",
|
|
132
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {} },
|
|
133
|
+
opts: { attempts: ATTEMPTS },
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("BullmqQueueTransportClient persistence", () => {
|
|
140
|
+
const jobs = [
|
|
141
|
+
{ queue: "orders", jobId: "outbox-1" },
|
|
142
|
+
{ queue: "orders", jobId: "outbox-2" },
|
|
143
|
+
{ queue: "billing", jobId: "outbox-3" },
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
it("waits once per queue for the append-only file, then finds every job still held", async () => {
|
|
147
|
+
const { client, queue, commands } = await build()
|
|
148
|
+
commands.call.mockResolvedValue([1, 0])
|
|
149
|
+
queue.getJobState.mockResolvedValue("waiting")
|
|
150
|
+
|
|
151
|
+
await expect(client.persist(jobs)).resolves.toBeUndefined()
|
|
152
|
+
|
|
153
|
+
expect(commands.call.mock.calls).toEqual([
|
|
154
|
+
["WAITAOF", 1, 0, 5000],
|
|
155
|
+
["WAITAOF", 1, 0, 5000],
|
|
156
|
+
])
|
|
157
|
+
expect(queue.getJobState.mock.calls).toEqual([["outbox-1"], ["outbox-2"], ["outbox-3"]])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it.each([[[0, 0]], ["OK"]])(
|
|
161
|
+
"refuses when Redis does not report the jobs written to its append-only file (%j)",
|
|
162
|
+
async (reply) => {
|
|
163
|
+
const { client, queue, commands } = await build()
|
|
164
|
+
commands.call.mockResolvedValue(reply)
|
|
165
|
+
|
|
166
|
+
const refusal = client.persist(jobs)
|
|
167
|
+
|
|
168
|
+
await expect(refusal).rejects.toBeInstanceOf(QueueError)
|
|
169
|
+
await expect(refusal).rejects.toMatchObject({
|
|
170
|
+
code: QueueErrorCode.NotPersisted,
|
|
171
|
+
params: { queue: "orders" },
|
|
172
|
+
})
|
|
173
|
+
expect(queue.getJobState).not.toHaveBeenCalled()
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
it("refuses when the queue's connection cannot send the wait at all", async () => {
|
|
178
|
+
const { client, queue } = await build()
|
|
179
|
+
queue.getBackend.mockReturnValue({ client: Promise.resolve({}) })
|
|
180
|
+
|
|
181
|
+
await expect(client.persist(jobs)).rejects.toMatchObject({ code: QueueErrorCode.NotPersisted })
|
|
182
|
+
expect(queue.getJobState).not.toHaveBeenCalled()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("refuses a job a restarted Redis no longer holds, although the wait answered on its new connection", async () => {
|
|
186
|
+
const { client, queue, commands } = await build()
|
|
187
|
+
commands.call.mockResolvedValue([1, 0])
|
|
188
|
+
queue.getJobState.mockImplementation((jobId) => Promise.resolve(jobId === "outbox-2" ? "unknown" : "waiting"))
|
|
189
|
+
|
|
190
|
+
const refusal = client.persist(jobs)
|
|
191
|
+
|
|
192
|
+
await expect(refusal).rejects.toBeInstanceOf(QueueError)
|
|
193
|
+
await expect(refusal).rejects.toMatchObject({
|
|
194
|
+
code: QueueErrorCode.JobLost,
|
|
195
|
+
params: { queue: "orders", jobId: "outbox-2" },
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe("BullmqQueueTransportClient worker deliveries", () => {
|
|
201
|
+
it("starts a worker that translates BullMQ jobs into queue deliveries", async () => {
|
|
202
|
+
const { client, factory } = await build()
|
|
203
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
204
|
+
handler.handle.mockResolvedValue(undefined)
|
|
205
|
+
|
|
206
|
+
await client.work(handler, 3)
|
|
207
|
+
|
|
208
|
+
expect(factory.worker).toHaveBeenCalledWith("orders", expect.any(Function), {
|
|
209
|
+
prefix: "spec.",
|
|
210
|
+
connection: { host: "redis.test", port: 6380, db: 7, maxRetriesPerRequest: null },
|
|
211
|
+
concurrency: 3,
|
|
212
|
+
})
|
|
213
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
214
|
+
await processor?.(
|
|
215
|
+
mock<BullmqJob>({
|
|
216
|
+
id: "job-1",
|
|
217
|
+
data: {
|
|
218
|
+
schema: BULLMQ_JOB_SCHEMA,
|
|
219
|
+
kind: "enqueued",
|
|
220
|
+
payload: { orderId: "order-1" },
|
|
221
|
+
correlationId: "request-1",
|
|
222
|
+
},
|
|
223
|
+
attemptsMade: 2,
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
await processor?.({
|
|
227
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: { orderId: "order-2" } },
|
|
228
|
+
attemptsMade: 0,
|
|
229
|
+
} satisfies BullmqJob)
|
|
230
|
+
await processor?.({
|
|
231
|
+
id: "tick-3",
|
|
232
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {} },
|
|
233
|
+
attemptsMade: 1,
|
|
234
|
+
} satisfies BullmqJob)
|
|
235
|
+
|
|
236
|
+
expect(handler.handle).toHaveBeenNthCalledWith(1, {
|
|
237
|
+
id: "job-1",
|
|
238
|
+
correlationId: "request-1",
|
|
239
|
+
queue: "orders",
|
|
240
|
+
payload: { orderId: "order-1" },
|
|
241
|
+
attempt: 3,
|
|
242
|
+
})
|
|
243
|
+
expect(handler.handle).toHaveBeenNthCalledWith(2, {
|
|
244
|
+
id: "",
|
|
245
|
+
correlationId: MINTED_ID,
|
|
246
|
+
queue: "orders",
|
|
247
|
+
payload: { orderId: "order-2" },
|
|
248
|
+
attempt: 1,
|
|
249
|
+
})
|
|
250
|
+
expect(handler.handle).toHaveBeenNthCalledWith(3, {
|
|
251
|
+
id: "tick-3",
|
|
252
|
+
correlationId: "tick-3",
|
|
253
|
+
queue: "orders",
|
|
254
|
+
payload: {},
|
|
255
|
+
attempt: 2,
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe("BullmqQueueTransportClient stored job upgrades", () => {
|
|
261
|
+
it("preserves raw payloads and the same job correlation through legacy retries", async () => {
|
|
262
|
+
const { client, factory, ids } = await build()
|
|
263
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
264
|
+
await client.work(handler, 3)
|
|
265
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
266
|
+
const payload = { orderId: "before-upgrade", kind: "expire-order", payload: { reason: "unpaid" } }
|
|
267
|
+
|
|
268
|
+
await processor?.({ id: "legacy-job", data: payload, attemptsMade: 0 })
|
|
269
|
+
await processor?.({ id: "legacy-job", data: payload, attemptsMade: 1 })
|
|
270
|
+
|
|
271
|
+
expect(handler.handle).toHaveBeenNthCalledWith(1, {
|
|
272
|
+
id: "legacy-job",
|
|
273
|
+
queue: "orders",
|
|
274
|
+
payload,
|
|
275
|
+
correlationId: "legacy-job",
|
|
276
|
+
attempt: 1,
|
|
277
|
+
})
|
|
278
|
+
expect(handler.handle).toHaveBeenNthCalledWith(2, {
|
|
279
|
+
id: "legacy-job",
|
|
280
|
+
queue: "orders",
|
|
281
|
+
payload,
|
|
282
|
+
correlationId: "legacy-job",
|
|
283
|
+
attempt: 2,
|
|
284
|
+
})
|
|
285
|
+
expect(ids.issued).toEqual([])
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it.each(["enqueued", "scheduled"])("keeps legacy business fields even when kind is %s", async (kind) => {
|
|
289
|
+
const { client, factory, ids } = await build()
|
|
290
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
291
|
+
await client.work(handler, 3)
|
|
292
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
293
|
+
const payload = {
|
|
294
|
+
schema: "order/business@1",
|
|
295
|
+
kind,
|
|
296
|
+
correlationId: "business\nkey",
|
|
297
|
+
payload: { orderId: "legacy-order" },
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await processor?.({ id: "legacy-collision", data: payload, attemptsMade: 0 })
|
|
301
|
+
|
|
302
|
+
expect(handler.handle).toHaveBeenCalledWith({
|
|
303
|
+
id: "legacy-collision",
|
|
304
|
+
queue: "orders",
|
|
305
|
+
payload,
|
|
306
|
+
correlationId: "legacy-collision",
|
|
307
|
+
attempt: 1,
|
|
308
|
+
})
|
|
309
|
+
expect(ids.issued).toEqual([])
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe("BullmqQueueTransportClient correlation roots and marked envelopes", () => {
|
|
314
|
+
it("mints only when legacy data has no job id and accepts explicit scheduled correlation", async () => {
|
|
315
|
+
const { client, factory, ids } = await build()
|
|
316
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
317
|
+
await client.work(handler, 3)
|
|
318
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
319
|
+
|
|
320
|
+
await processor?.({ data: { orderId: "legacy-no-id" }, attemptsMade: 0 })
|
|
321
|
+
await processor?.({
|
|
322
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {}, correlationId: "upstream-tick" },
|
|
323
|
+
attemptsMade: 0,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
expect(handler.handle.mock.calls.map(([delivery]) => delivery.correlationId)).toEqual([
|
|
327
|
+
MINTED_ID,
|
|
328
|
+
"upstream-tick",
|
|
329
|
+
])
|
|
330
|
+
expect(ids.issued).toEqual([MINTED_ID])
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it.each<unknown>([
|
|
334
|
+
null,
|
|
335
|
+
{ schema: "starci/queue@2", kind: "enqueued", payload: {}, correlationId: "request-1" },
|
|
336
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "unknown", payload: {} },
|
|
337
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: {}, correlationId: undefined },
|
|
338
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: null, correlationId: "request-1" },
|
|
339
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: {}, correlationId: "bad\nidentity" },
|
|
340
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {}, correlationId: undefined },
|
|
341
|
+
])("rejects malformed current data before business delivery (%j)", async (data) => {
|
|
342
|
+
const { client, factory } = await build()
|
|
343
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
344
|
+
await client.work(handler, 3)
|
|
345
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
346
|
+
|
|
347
|
+
await expect(processor?.({ id: "stored-job", data, attemptsMade: 0 })).rejects.toMatchObject({
|
|
348
|
+
code: QueueErrorCode.MalformedMessage,
|
|
349
|
+
})
|
|
350
|
+
expect(handler.handle).not.toHaveBeenCalled()
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe("BullmqQueueTransportClient driver errors", () => {
|
|
355
|
+
it("routes queue driver errors through the logger once per queue", async () => {
|
|
356
|
+
const { client, queue, logger } = await build()
|
|
357
|
+
await client.add("orders", "outbox-1", {}, "request-1")
|
|
358
|
+
await client.add("orders", "outbox-2", {}, "request-2")
|
|
359
|
+
const cause = new Error("redis connection lost")
|
|
360
|
+
|
|
361
|
+
const listener = queue.on.mock.calls[0]?.[1]
|
|
362
|
+
listener?.(cause)
|
|
363
|
+
|
|
364
|
+
expect(queue.on).toHaveBeenCalledTimes(1)
|
|
365
|
+
expect(queue.on).toHaveBeenCalledWith("error", expect.any(Function))
|
|
366
|
+
expect(logger.error).toHaveBeenCalledWith(QueueLogEvent.QueueFailed, cause, { queue: "orders" })
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it("routes worker driver errors through the logger", async () => {
|
|
370
|
+
const { client, worker, logger } = await build()
|
|
371
|
+
await client.work(mock<QueueHandler>({ queue: "orders" }), 3)
|
|
372
|
+
const cause = new Error("blocking redis connection lost")
|
|
373
|
+
|
|
374
|
+
const listener = worker.on.mock.calls[0]?.[1]
|
|
375
|
+
listener?.(cause)
|
|
376
|
+
|
|
377
|
+
expect(worker.on).toHaveBeenCalledWith("error", expect.any(Function))
|
|
378
|
+
expect(logger.error).toHaveBeenCalledWith(QueueLogEvent.WorkerFailed, cause, { queue: "orders" })
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe("BullmqQueueTransportClient wait and shutdown", () => {
|
|
383
|
+
it("waits for the requested delay", async () => {
|
|
384
|
+
jest.useFakeTimers()
|
|
385
|
+
try {
|
|
386
|
+
const { client } = await build()
|
|
387
|
+
|
|
388
|
+
const waiting = client.wait(250)
|
|
389
|
+
await jest.advanceTimersByTimeAsync(250)
|
|
390
|
+
|
|
391
|
+
await expect(waiting).resolves.toBeUndefined()
|
|
392
|
+
} finally {
|
|
393
|
+
jest.useRealTimers()
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it("closes workers before queues", async () => {
|
|
398
|
+
const { client, queue, worker } = await build()
|
|
399
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
400
|
+
let finishWorkerClose: (() => void) | undefined
|
|
401
|
+
worker.close.mockReturnValue(
|
|
402
|
+
new Promise((resolve) => {
|
|
403
|
+
finishWorkerClose = resolve
|
|
404
|
+
}),
|
|
405
|
+
)
|
|
406
|
+
await client.add("orders", "outbox-1", {}, "request-1")
|
|
407
|
+
await client.work(handler, 3)
|
|
408
|
+
|
|
409
|
+
const closing = client.close()
|
|
410
|
+
|
|
411
|
+
expect(worker.close).toHaveBeenCalledWith(true)
|
|
412
|
+
expect(queue.close).not.toHaveBeenCalled()
|
|
413
|
+
finishWorkerClose?.()
|
|
414
|
+
await closing
|
|
415
|
+
expect(queue.close).toHaveBeenCalledTimes(1)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import { eachInOrder, 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"
|
|
7
|
+
import { QueueError, QueueErrorCode } from "./errors/queue.error"
|
|
8
|
+
import { InjectQueueFactory, InjectQueueOptions } from "./queue.decorators"
|
|
9
|
+
import { BULLMQ_JOB_SCHEMA } from "./queue.contracts"
|
|
10
|
+
import type { QueueDelivery, QueueHandler, QueueSchedulerDefinition } from "./queue.contracts"
|
|
11
|
+
import { QueueLogEvent } from "./queue.log-events"
|
|
12
|
+
import type { QueueOptions } from "./queue.options"
|
|
13
|
+
import { ATTEMPTS, BACKOFF_MS, KEEP_COMPLETED_SECONDS, jobNameOf } from "./queue.policy"
|
|
14
|
+
import type {
|
|
15
|
+
BullmqCommands,
|
|
16
|
+
BullmqConnection,
|
|
17
|
+
BullmqJob,
|
|
18
|
+
BullmqQueue,
|
|
19
|
+
BullmqWorker,
|
|
20
|
+
QueuedJob,
|
|
21
|
+
QueueFactory,
|
|
22
|
+
QueueTransport,
|
|
23
|
+
} from "./queue-transport.port"
|
|
24
|
+
|
|
25
|
+
/** How long the relay waits for Redis to write its jobs to the append-only file; `appendfsync everysec` writes within a second. */
|
|
26
|
+
const PERSIST_TIMEOUT_MS = 5000
|
|
27
|
+
|
|
28
|
+
/** A WAITAOF reply that counts the local append-only file among the writes it waited for. */
|
|
29
|
+
const writtenLocally = (reply: unknown): boolean => Array.isArray(reply) && Number(reply.at(0)) >= 1
|
|
30
|
+
|
|
31
|
+
/** True for a connection that sends a raw command. */
|
|
32
|
+
const sendsCommands = (client: object): client is BullmqCommands =>
|
|
33
|
+
"call" in client && typeof client.call === "function"
|
|
34
|
+
|
|
35
|
+
@Injectable()
|
|
36
|
+
/** The QueueTransport port over the declared Redis the app names in its options; the BullMQ objects come from the injected factory the module composes. */
|
|
37
|
+
export class BullmqQueueTransportClient implements QueueTransport {
|
|
38
|
+
private readonly queues = new Map<string, BullmqQueue>()
|
|
39
|
+
private readonly workers: Array<BullmqWorker> = []
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
@InjectQueueOptions() private readonly options: QueueOptions,
|
|
43
|
+
@InjectQueueFactory() private readonly factory: QueueFactory,
|
|
44
|
+
@InjectIds() private readonly ids: Ids,
|
|
45
|
+
@InjectLogger() private readonly logger: Logger,
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/** Adds the job with the outbox row id as its BullMQ id, so adding it twice leaves one job. */
|
|
49
|
+
async add(queue: string, jobId: string, payload: object, correlationId: string): Promise<void> {
|
|
50
|
+
if (!isCorrelationId(correlationId)) throw new QueueError({ code: QueueErrorCode.CorrelationInvalid })
|
|
51
|
+
await this.queueOf(queue).add(
|
|
52
|
+
jobNameOf(queue),
|
|
53
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload, correlationId },
|
|
54
|
+
{
|
|
55
|
+
jobId,
|
|
56
|
+
attempts: ATTEMPTS,
|
|
57
|
+
backoff: { type: "exponential", delay: BACKOFF_MS },
|
|
58
|
+
removeOnComplete: { age: KEEP_COMPLETED_SECONDS },
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Waits until Redis has written each queue's jobs to its append-only file, then checks it still holds every job: a Redis that
|
|
65
|
+
* restarted meanwhile answers the wait at once on its new connection, and only a job it reloaded from the file is still there.
|
|
66
|
+
*/
|
|
67
|
+
async persist(jobs: ReadonlyArray<QueuedJob>): Promise<void> {
|
|
68
|
+
await eachInOrder([...new Set(jobs.map((job) => job.queue))], async (queue) => {
|
|
69
|
+
const client = await this.queueOf(queue).getBackend().client
|
|
70
|
+
const written =
|
|
71
|
+
sendsCommands(client) && writtenLocally(await client.call("WAITAOF", 1, 0, PERSIST_TIMEOUT_MS))
|
|
72
|
+
if (!written) throw new QueueError({ code: QueueErrorCode.NotPersisted, params: { queue } })
|
|
73
|
+
})
|
|
74
|
+
await eachInOrder(jobs, async (job) => {
|
|
75
|
+
if ((await this.queueOf(job.queue).getJobState(job.jobId)) === "unknown")
|
|
76
|
+
throw new QueueError({ code: QueueErrorCode.JobLost, params: { queue: job.queue, jobId: job.jobId } })
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Creates or updates the scheduler; BullMQ keeps one per id however many replicas call this. */
|
|
81
|
+
async upsertScheduler(scheduler: QueueSchedulerDefinition): Promise<void> {
|
|
82
|
+
await this.queueOf(scheduler.queue).upsertJobScheduler(
|
|
83
|
+
scheduler.id,
|
|
84
|
+
{ every: scheduler.everyMs },
|
|
85
|
+
{
|
|
86
|
+
name: jobNameOf(scheduler.queue),
|
|
87
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: scheduler.payload ?? {} },
|
|
88
|
+
opts: { attempts: ATTEMPTS },
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Starts a worker of the queue. */
|
|
94
|
+
work(handler: QueueHandler, concurrency: number): Promise<void> {
|
|
95
|
+
const worker = this.factory.worker(
|
|
96
|
+
handler.queue,
|
|
97
|
+
async (job) => {
|
|
98
|
+
await handler.handle(this.deliveryOf(handler.queue, job))
|
|
99
|
+
},
|
|
100
|
+
{ ...this.connection(), concurrency },
|
|
101
|
+
)
|
|
102
|
+
worker.on("error", (cause) => this.logger.error(QueueLogEvent.WorkerFailed, cause, { queue: handler.queue }))
|
|
103
|
+
this.workers.push(worker)
|
|
104
|
+
return Promise.resolve()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Resolves after `ms` milliseconds. */
|
|
108
|
+
wait(ms: number): Promise<void> {
|
|
109
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Closes the workers first, then the queues. */
|
|
113
|
+
async close(): Promise<void> {
|
|
114
|
+
await Promise.all(this.workers.map((worker) => worker.close(true)))
|
|
115
|
+
await Promise.all([...this.queues.values()].map((queue) => queue.close()))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Reads current envelopes and stored raw payloads without changing the identity of a retried job. */
|
|
119
|
+
private deliveryOf(queue: string, job: BullmqJob): QueueDelivery {
|
|
120
|
+
const data = job.data
|
|
121
|
+
if (!isRecord(data)) throw new QueueError({ code: QueueErrorCode.MalformedMessage })
|
|
122
|
+
let payload: object = data
|
|
123
|
+
let correlationId = job.id
|
|
124
|
+
const schema = data.schema
|
|
125
|
+
const schemaPrefix = BULLMQ_JOB_SCHEMA.slice(0, BULLMQ_JOB_SCHEMA.lastIndexOf("@") + 1)
|
|
126
|
+
if (typeof schema === "string" && schema.startsWith(schemaPrefix)) {
|
|
127
|
+
if (
|
|
128
|
+
schema !== BULLMQ_JOB_SCHEMA ||
|
|
129
|
+
(data.kind !== "enqueued" && data.kind !== "scheduled") ||
|
|
130
|
+
!isRecord(data.payload)
|
|
131
|
+
) {
|
|
132
|
+
throw new QueueError({ code: QueueErrorCode.MalformedMessage })
|
|
133
|
+
}
|
|
134
|
+
payload = data.payload
|
|
135
|
+
if (data.kind === "enqueued" || "correlationId" in data) {
|
|
136
|
+
if (!isCorrelationId(data.correlationId)) {
|
|
137
|
+
throw new QueueError({ code: QueueErrorCode.MalformedMessage })
|
|
138
|
+
}
|
|
139
|
+
correlationId = data.correlationId
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id: job.id ?? "",
|
|
144
|
+
queue,
|
|
145
|
+
correlationId: correlationId ?? this.ids.next(),
|
|
146
|
+
payload,
|
|
147
|
+
attempt: job.attemptsMade + 1,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private connection(): BullmqConnection {
|
|
152
|
+
return {
|
|
153
|
+
prefix: this.options.prefix,
|
|
154
|
+
connection: {
|
|
155
|
+
host: this.options.redisHost,
|
|
156
|
+
port: this.options.redisPort,
|
|
157
|
+
db: this.options.redisDb,
|
|
158
|
+
...(this.options.redisPassword === undefined ? {} : { password: this.options.redisPassword.reveal() }),
|
|
159
|
+
maxRetriesPerRequest: null,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private queueOf(name: string): BullmqQueue {
|
|
165
|
+
const known = this.queues.get(name)
|
|
166
|
+
if (known !== undefined) return known
|
|
167
|
+
const created = this.factory.queue(name, this.connection())
|
|
168
|
+
created.on("error", (cause) => this.logger.error(QueueLogEvent.QueueFailed, cause, { queue: name }))
|
|
169
|
+
this.queues.set(name, created)
|
|
170
|
+
return created
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { sql } from "@modules/platform/database"
|
|
2
|
+
|
|
3
|
+
/** Writes one job in the outbox ($1 queue, $2 payload, $3 instant, $4 correlation id); the relay hands it to BullMQ after the commit. */
|
|
4
|
+
export const INSERT_QUEUE_ROW = sql`INSERT INTO public.queue (queue, payload, created_at, correlation_id)
|
|
5
|
+
VALUES ($1, $2::jsonb, $3, $4)`
|
|
6
|
+
|
|
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, coalesce(correlation_id, id::text) AS "correlationId" FROM public.queue
|
|
9
|
+
WHERE sent_at IS NULL ORDER BY created_at, id LIMIT $1 FOR UPDATE SKIP LOCKED`
|
|
10
|
+
|
|
11
|
+
/** Marks the rows as handed to BullMQ ($1 ids, $2 instant). */
|
|
12
|
+
export const MARK_ROWS_SENT = sql`UPDATE public.queue SET sent_at = $2 WHERE id = ANY($1::uuid[])`
|