@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
package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
|
|
3
|
+
import { requestFingerprint } from "./request-fingerprint.policy"
|
|
4
|
+
|
|
5
|
+
const FINGERPRINT = Symbol("fingerprint")
|
|
6
|
+
const build = async () => {
|
|
7
|
+
const moduleRef = await Test.createTestingModule({
|
|
8
|
+
providers: [{ provide: FINGERPRINT, useValue: requestFingerprint }],
|
|
9
|
+
}).compile()
|
|
10
|
+
return moduleRef.get<typeof requestFingerprint>(FINGERPRINT)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("requestFingerprint", () => {
|
|
14
|
+
it("hashes nested objects independently of property insertion order", async () => {
|
|
15
|
+
const fingerprint = await build()
|
|
16
|
+
|
|
17
|
+
expect(fingerprint({ personId: "person-1", body: { quantity: 2, productId: "product-1" } })).toBe(
|
|
18
|
+
fingerprint({ body: { productId: "product-1", quantity: 2 }, personId: "person-1" }),
|
|
19
|
+
)
|
|
20
|
+
expect(fingerprint({ a: 1 })).toMatch(/^[a-f0-9]{64}$/)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("keeps binary UTF-16 key order for non-ASCII and supplementary Unicode keys", async () => {
|
|
24
|
+
const fingerprint = await build()
|
|
25
|
+
const body = { "\uE000": 5, "\u{1F600}": 4, "\u00E9": 1, a: 3, Z: 2 }
|
|
26
|
+
|
|
27
|
+
expect(fingerprint(body)).toBe(fingerprint({ Z: 2, a: 3, "\u00E9": 1, "\u{1F600}": 4, "\uE000": 5 }))
|
|
28
|
+
expect(fingerprint(body)).toBe("6eb3469044da5c7df68404ba05b90306384fc773ad605d4de06321a2092ff365")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("preserves array order, JSON types and nested values", async () => {
|
|
32
|
+
const fingerprint = await build()
|
|
33
|
+
|
|
34
|
+
expect(fingerprint([1, 2])).not.toBe(fingerprint([2, 1]))
|
|
35
|
+
expect(fingerprint({ value: 1 })).not.toBe(fingerprint({ value: "1" }))
|
|
36
|
+
expect(fingerprint({ body: { quantity: 2 } })).not.toBe(fingerprint({ body: { quantity: 3 } }))
|
|
37
|
+
expect(new Set([null, true, false, "", 0].map(fingerprint)).size).toBe(5)
|
|
38
|
+
expect(fingerprint([])).not.toBe(fingerprint({}))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("accepts an object without a prototype and repeated noncyclic references", async () => {
|
|
42
|
+
const fingerprint = await build()
|
|
43
|
+
const shared = { quantity: 2 }
|
|
44
|
+
const withoutPrototype: unknown = Object.assign(Object.create(null), { quantity: 2 })
|
|
45
|
+
|
|
46
|
+
expect(fingerprint(withoutPrototype)).toBe(fingerprint(shared))
|
|
47
|
+
expect(fingerprint([shared, shared])).toBe(fingerprint([{ quantity: 2 }, { quantity: 2 }]))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it.each([
|
|
51
|
+
[undefined],
|
|
52
|
+
[() => 1],
|
|
53
|
+
[Symbol("unsupported")],
|
|
54
|
+
[BigInt(1)],
|
|
55
|
+
[Number.NaN],
|
|
56
|
+
[Number.POSITIVE_INFINITY],
|
|
57
|
+
[Number.NEGATIVE_INFINITY],
|
|
58
|
+
[{ missing: undefined }],
|
|
59
|
+
[[undefined]],
|
|
60
|
+
[Array(1)],
|
|
61
|
+
[new Date("2026-10-24T00:00:00.000Z")],
|
|
62
|
+
[new Map([["quantity", 2]])],
|
|
63
|
+
])("refuses values that cannot represent a JSON request: %p", async (request: unknown) => {
|
|
64
|
+
const fingerprint = await build()
|
|
65
|
+
|
|
66
|
+
expect(() => fingerprint(request)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("refuses cyclic requests before a claim can be written", async () => {
|
|
70
|
+
const fingerprint = await build()
|
|
71
|
+
const cyclic: Record<string, unknown> = { quantity: 2 }
|
|
72
|
+
cyclic.body = cyclic
|
|
73
|
+
|
|
74
|
+
expect(() => fingerprint(cyclic)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
|
|
3
|
+
|
|
4
|
+
/** Serializes JSON without depending on object insertion order; array order remains part of the request. */
|
|
5
|
+
const serializeJson = (value: unknown, ancestors: Set<object>): string => {
|
|
6
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value)
|
|
7
|
+
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value)
|
|
8
|
+
if (typeof value !== "object" || ancestors.has(value))
|
|
9
|
+
throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
|
|
10
|
+
ancestors.add(value)
|
|
11
|
+
let serialized: string
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
serialized = `[${Array.from(value, (item: unknown) => serializeJson(item, ancestors)).join(",")}]`
|
|
14
|
+
} else {
|
|
15
|
+
const prototype: unknown = Object.getPrototypeOf(value)
|
|
16
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
17
|
+
throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
|
|
18
|
+
}
|
|
19
|
+
serialized = `{${Object.keys(value)
|
|
20
|
+
.sort((left, right) => Number(left > right) - Number(left < right))
|
|
21
|
+
.map((key) => {
|
|
22
|
+
const item: unknown = Reflect.get(value, key)
|
|
23
|
+
return `${JSON.stringify(key)}:${serializeJson(item, ancestors)}`
|
|
24
|
+
})
|
|
25
|
+
.join(",")}}`
|
|
26
|
+
}
|
|
27
|
+
ancestors.delete(value)
|
|
28
|
+
return serialized
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Returns a stable JSON representation for both the request digest and the stored outcome. */
|
|
32
|
+
export const canonicalJson = (value: unknown): string => serializeJson(value, new Set())
|
|
33
|
+
|
|
34
|
+
/** Hashes the validated client request, so a reused key cannot silently apply a different body. */
|
|
35
|
+
export const requestFingerprint = (request: unknown): string =>
|
|
36
|
+
createHash("sha256").update(canonicalJson(request)).digest("hex")
|
|
@@ -32,7 +32,7 @@ const buildWithoutConnection = async () => {
|
|
|
32
32
|
return { claims: moduleRef.get(JobClaimService) }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const claimed: ClaimedJob = { jobId: "j-1", kind: "mail", fencingToken: 3, currentStep: null, payload: { id: "m-1" } }
|
|
35
|
+
const claimed: ClaimedJob = { correlationId: "request-1", jobId: "j-1", kind: "mail", fencingToken: 3, currentStep: null, payload: { id: "m-1" } }
|
|
36
36
|
|
|
37
37
|
describe("JobClaimService", () => {
|
|
38
38
|
describe("claim", () => {
|
|
@@ -48,6 +48,7 @@ describe("JobClaimService", () => {
|
|
|
48
48
|
const job = await claims.claim({
|
|
49
49
|
kind: "mail",
|
|
50
50
|
jobKey: "k-1",
|
|
51
|
+
correlationId: "request-1",
|
|
51
52
|
payload: { id: "m-1" },
|
|
52
53
|
workerId: "w-1",
|
|
53
54
|
leaseMs: 60000,
|
|
@@ -69,7 +70,7 @@ describe("JobClaimService", () => {
|
|
|
69
70
|
const { claims } = await build(manager)
|
|
70
71
|
|
|
71
72
|
await expect(
|
|
72
|
-
claims.claim({ kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
|
|
73
|
+
claims.claim({ correlationId: "request-1", kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
|
|
73
74
|
).resolves.toBeNull()
|
|
74
75
|
})
|
|
75
76
|
})
|
|
@@ -141,7 +142,7 @@ describe("JobClaimService", () => {
|
|
|
141
142
|
const { claims } = await buildWithoutConnection()
|
|
142
143
|
|
|
143
144
|
await expect(
|
|
144
|
-
claims.claim({ kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
|
|
145
|
+
claims.claim({ correlationId: "request-1", kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
|
|
145
146
|
).rejects.toMatchObject({
|
|
146
147
|
code: JobsErrorCode.ConnectionMissing,
|
|
147
148
|
})
|
|
@@ -44,8 +44,10 @@ export class JobClaimService implements JobClaims {
|
|
|
44
44
|
])
|
|
45
45
|
const row = rows[0]
|
|
46
46
|
if (row === undefined) return null
|
|
47
|
+
// The durable delivery supplies the same request id on every retry; the job fence identifies a claim separately.
|
|
47
48
|
return {
|
|
48
49
|
jobId: row.id,
|
|
50
|
+
correlationId: params.correlationId,
|
|
49
51
|
kind: row.kind,
|
|
50
52
|
fencingToken: Number(row.fencing_token),
|
|
51
53
|
currentStep: row.current_step,
|
|
@@ -14,8 +14,8 @@ import type { JobClaims } from "./jobs.port"
|
|
|
14
14
|
import { JobRunnerService } from "./job-runner.service"
|
|
15
15
|
|
|
16
16
|
const options: JobsOptions = { connection: "primary", workerId: "w-1", leaseMs: 60000 }
|
|
17
|
-
const delivery: QueueDelivery = { id: "k-1", queue: "mail", payload: { id: "m-1" }, attempt: 1 }
|
|
18
|
-
const job: ClaimedJob = { jobId: "j-1", kind: "mail", fencingToken: 2, currentStep: null, payload: { id: "m-1" } }
|
|
17
|
+
const delivery: QueueDelivery = { correlationId: "request-1", id: "k-1", queue: "mail", payload: { id: "m-1" }, attempt: 1 }
|
|
18
|
+
const job: ClaimedJob = { correlationId: "request-1", jobId: "j-1", kind: "mail", fencingToken: 2, currentStep: null, payload: { id: "m-1" } }
|
|
19
19
|
const fenced = (): JobsError => new JobsError({ code: JobsErrorCode.FencedOut, params: { jobId: "j-1", token: 2 } })
|
|
20
20
|
|
|
21
21
|
const build = async () => {
|
|
@@ -66,6 +66,7 @@ describe("JobRunnerService", () => {
|
|
|
66
66
|
expect(claims.claim).toHaveBeenCalledWith({
|
|
67
67
|
kind: "mail",
|
|
68
68
|
jobKey: "k-1",
|
|
69
|
+
correlationId: "request-1",
|
|
69
70
|
payload: { id: "m-1" },
|
|
70
71
|
workerId: "w-1",
|
|
71
72
|
leaseMs: 60000,
|
|
@@ -75,6 +76,23 @@ describe("JobRunnerService", () => {
|
|
|
75
76
|
expect(claims.fail).not.toHaveBeenCalled()
|
|
76
77
|
})
|
|
77
78
|
|
|
79
|
+
it("retains the original request correlation when a retry claims a newer fence", async () => {
|
|
80
|
+
const { runner, claims, processor } = await build()
|
|
81
|
+
const failure = new Error("provider unavailable")
|
|
82
|
+
claims.claim.mockResolvedValueOnce(job).mockResolvedValueOnce({ ...job, fencingToken: 3 })
|
|
83
|
+
processor.process.mockRejectedValueOnce(failure).mockResolvedValueOnce(undefined)
|
|
84
|
+
|
|
85
|
+
await expect(runner.run(processor, delivery)).rejects.toBe(failure)
|
|
86
|
+
await runner.run(processor, { ...delivery, attempt: 2 })
|
|
87
|
+
|
|
88
|
+
expect(processor.process.mock.calls.map(([claimed]) => claimed.correlationId)).toEqual([
|
|
89
|
+
"request-1",
|
|
90
|
+
"request-1",
|
|
91
|
+
])
|
|
92
|
+
expect(processor.process.mock.calls.map(([claimed]) => claimed.fencingToken)).toEqual([2, 3])
|
|
93
|
+
expect(claims.complete).toHaveBeenCalledWith({ jobId: "j-1", expectedFencingToken: 3 })
|
|
94
|
+
})
|
|
95
|
+
|
|
78
96
|
it("does nothing when the job is done or another worker holds a live claim", async () => {
|
|
79
97
|
const { runner, claims, processor } = await build()
|
|
80
98
|
claims.claim.mockResolvedValue(null)
|
|
@@ -38,6 +38,7 @@ export class JobRunnerService implements JobProcessorRegistry {
|
|
|
38
38
|
const job = await this.claims.claim({
|
|
39
39
|
kind: processor.queue,
|
|
40
40
|
jobKey: delivery.id,
|
|
41
|
+
correlationId: delivery.correlationId,
|
|
41
42
|
payload: delivery.payload,
|
|
42
43
|
workerId: this.options.workerId,
|
|
43
44
|
leaseMs: this.options.leaseMs,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** A job a worker claimed: the token it must pass to every write. */
|
|
2
2
|
export interface ClaimedJob {
|
|
3
|
+
/** The originating request id carried explicitly by the durable queue delivery. */
|
|
4
|
+
readonly correlationId: string
|
|
3
5
|
/** The id of the job row. */
|
|
4
6
|
readonly jobId: string
|
|
5
7
|
/** The kind of the job: the queue its processor consumes. */
|
|
@@ -14,6 +16,8 @@ export interface ClaimedJob {
|
|
|
14
16
|
|
|
15
17
|
/** What a worker asks for when it claims a delivery. */
|
|
16
18
|
export interface ClaimParams {
|
|
19
|
+
/** The originating request id retained across delivery retries. */
|
|
20
|
+
readonly correlationId: string
|
|
17
21
|
/** The kind of the job. */
|
|
18
22
|
readonly kind: string
|
|
19
23
|
/** The key of the delivery: the BullMQ job id. A second claim of the same key bumps the token of the same row. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { JOBS_ERROR_KINDS } from "./errors/jobs.error"
|
|
2
|
+
export { parseJobsConfig } from "./jobs.config"
|
|
3
|
+
export type { JobsConfig } from "./jobs.config"
|
|
4
|
+
export { isRunKey } from "./jobs.contracts"
|
|
5
|
+
export type { ClaimedJob, RunKey } from "./jobs.contracts"
|
|
6
|
+
export { InjectJobClaims, InjectJobProcessorRegistry } from "./jobs.decorators"
|
|
7
|
+
export { JobsModule } from "./jobs.module"
|
|
8
|
+
export type { JobClaims, JobProcessorRegistry, JobStep } from "./jobs.port"
|
|
9
|
+
export { FencedProcessor } from "./fenced.processor"
|
|
10
|
+
export { JOBS_MESSAGES } from "./messages/jobs.messages"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- The fenced jobs: one row per delivery key. A claim bumps the fencing token and every
|
|
2
|
+
-- other write of a worker carries the token it holds, so a worker that lost its claim
|
|
3
|
+
-- changes nothing. Only the app_be connection reaches it.
|
|
4
|
+
create table public.jobs (
|
|
5
|
+
id uuid primary key default gen_random_uuid(),
|
|
6
|
+
kind varchar(200) not null,
|
|
7
|
+
job_key varchar(200) not null,
|
|
8
|
+
status varchar(16) not null,
|
|
9
|
+
current_step varchar(200),
|
|
10
|
+
fencing_token bigint not null,
|
|
11
|
+
claimed_by varchar(200),
|
|
12
|
+
lease_expires_at timestamptz,
|
|
13
|
+
payload jsonb not null,
|
|
14
|
+
error text,
|
|
15
|
+
created_at timestamptz not null,
|
|
16
|
+
updated_at timestamptz not null,
|
|
17
|
+
constraint jobs_job_key_key unique (job_key),
|
|
18
|
+
constraint jobs_status_check check (status in ('running', 'failed', 'done')),
|
|
19
|
+
constraint jobs_fencing_token_check check (fencing_token >= 1)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
alter table public.jobs enable row level security;
|
|
23
|
+
alter table public.jobs force row level security;
|
|
24
|
+
|
|
25
|
+
create trigger jobs_set_updated_at
|
|
26
|
+
before update on public.jobs
|
|
27
|
+
for each row
|
|
28
|
+
execute function private.set_updated_at();
|
|
29
|
+
|
|
30
|
+
create policy jobs_app_be_select
|
|
31
|
+
on public.jobs for select to app_be
|
|
32
|
+
using (kind is not null);
|
|
33
|
+
|
|
34
|
+
create policy jobs_app_be_insert
|
|
35
|
+
on public.jobs for insert to app_be
|
|
36
|
+
with check (kind is not null);
|
|
37
|
+
|
|
38
|
+
create policy jobs_app_be_update
|
|
39
|
+
on public.jobs for update to app_be
|
|
40
|
+
using (kind is not null)
|
|
41
|
+
with check (kind is not null);
|
|
42
|
+
|
|
43
|
+
revoke all on table public.jobs from public, anon, authenticated;
|
|
44
|
+
grant select, insert, update on table public.jobs to app_be;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sql } from "@modules/platform/database"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claims a delivery in one statement ($1 kind, $2 job key, $3 worker, $4 lease end, $5 payload, $6 instant). A new key inserts the row
|
|
5
|
+
* with token 1; an existing key is claimed again only when the job failed or its lease expired, and then the token is bumped by the
|
|
6
|
+
* store. A done job and a live claim match nothing, so no row comes back.
|
|
7
|
+
*/
|
|
8
|
+
export const CLAIM_JOB = sql`INSERT INTO public.jobs (kind, job_key, status, fencing_token, claimed_by, lease_expires_at, payload, created_at, updated_at)
|
|
9
|
+
VALUES ($1, $2, 'running', 1, $3, $4, $5::jsonb, $6, $6)
|
|
10
|
+
ON CONFLICT (job_key) DO UPDATE
|
|
11
|
+
SET fencing_token = jobs.fencing_token + 1, claimed_by = $3, lease_expires_at = $4, status = 'running', error = NULL, updated_at = $6
|
|
12
|
+
WHERE jobs.status = 'failed' OR (jobs.status = 'running' AND jobs.lease_expires_at < $6)
|
|
13
|
+
RETURNING id, kind, fencing_token, current_step, payload`
|
|
14
|
+
|
|
15
|
+
/** The guarded writes wrap their UPDATE in a CTE: TypeORM returns a bare `UPDATE ... RETURNING` as `[rows, count]`, a SELECT as rows. */
|
|
16
|
+
|
|
17
|
+
/** Records a step ($1 job id, $2 expected token, $3 step, $4 instant); no row comes back when the token is stale. */
|
|
18
|
+
export const ADVANCE_JOB = sql`WITH changed AS (UPDATE public.jobs SET current_step = $3, updated_at = $4
|
|
19
|
+
WHERE id = $1 AND fencing_token = $2 AND status = 'running'
|
|
20
|
+
RETURNING id)
|
|
21
|
+
SELECT id FROM changed`
|
|
22
|
+
|
|
23
|
+
/** Completes the job ($1 job id, $2 expected token, $3 instant). */
|
|
24
|
+
export const COMPLETE_JOB = sql`WITH changed AS (UPDATE public.jobs SET status = 'done', lease_expires_at = NULL, updated_at = $3
|
|
25
|
+
WHERE id = $1 AND fencing_token = $2 AND status = 'running'
|
|
26
|
+
RETURNING id)
|
|
27
|
+
SELECT id FROM changed`
|
|
28
|
+
|
|
29
|
+
/** Fails the job ($1 job id, $2 expected token, $3 reason, $4 instant). */
|
|
30
|
+
export const FAIL_JOB = sql`WITH changed AS (UPDATE public.jobs SET status = 'failed', error = $3, lease_expires_at = NULL, updated_at = $4
|
|
31
|
+
WHERE id = $1 AND fencing_token = $2 AND status = 'running'
|
|
32
|
+
RETURNING id)
|
|
33
|
+
SELECT id FROM changed`
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Test } from "@nestjs/testing"
|
|
2
|
-
import { mock } from "@starci/jest-preset"
|
|
2
|
+
import { fakeIds, mock } from "@starci/jest-preset"
|
|
3
|
+
import { LOGGER } from "@modules/platform/logging"
|
|
4
|
+
import type { Logger } from "@modules/platform/logging"
|
|
5
|
+
import { IDS } from "@modules/platform/ids"
|
|
6
|
+
import { QueueErrorCode } from "./errors/queue.error"
|
|
3
7
|
import { BullmqQueueTransportClient } from "./bullmq-queue-transport.client"
|
|
8
|
+
import { BULLMQ_JOB_SCHEMA } from "./queue.contracts"
|
|
4
9
|
import type { QueueHandler } from "./queue.contracts"
|
|
5
10
|
import { QUEUE_FACTORY, QUEUE_OPTIONS } from "./queue.decorators"
|
|
11
|
+
import { QueueLogEvent } from "./queue.log-events"
|
|
6
12
|
import type { QueueOptions } from "./queue.options"
|
|
7
13
|
import { ATTEMPTS, BACKOFF_MS, KEEP_COMPLETED_SECONDS } from "./queue.policy"
|
|
8
14
|
import type { BullmqJob, BullmqQueue, BullmqWorker, QueueFactory } from "./queue-transport.port"
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
const anyOf = (type: unknown): unknown => expect.any(type)
|
|
16
|
+
const MINTED_ID = "00000000-0000-4000-8000-000000000001"
|
|
12
17
|
|
|
13
18
|
const options: QueueOptions = {
|
|
14
19
|
redisHost: "redis.test",
|
|
@@ -26,6 +31,8 @@ const build = async () => {
|
|
|
26
31
|
const queue = mock<BullmqQueue>()
|
|
27
32
|
const worker = mock<BullmqWorker>()
|
|
28
33
|
const factory = mock<QueueFactory>()
|
|
34
|
+
const ids = fakeIds()
|
|
35
|
+
const logger = mock<Logger>()
|
|
29
36
|
factory.queue.mockReturnValue(queue)
|
|
30
37
|
factory.worker.mockReturnValue(worker)
|
|
31
38
|
const moduleRef = await Test.createTestingModule({
|
|
@@ -33,30 +40,46 @@ const build = async () => {
|
|
|
33
40
|
BullmqQueueTransportClient,
|
|
34
41
|
{ provide: QUEUE_OPTIONS, useValue: options },
|
|
35
42
|
{ provide: QUEUE_FACTORY, useValue: factory },
|
|
43
|
+
{ provide: IDS, useValue: ids },
|
|
44
|
+
{ provide: LOGGER, useValue: logger },
|
|
36
45
|
],
|
|
37
46
|
}).compile()
|
|
38
|
-
return { client: moduleRef.get(BullmqQueueTransportClient), queue, worker, factory }
|
|
47
|
+
return { client: moduleRef.get(BullmqQueueTransportClient), queue, worker, factory, logger, ids }
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
describe("BullmqQueueTransportClient", () => {
|
|
50
|
+
describe("BullmqQueueTransportClient jobs and schedulers", () => {
|
|
42
51
|
it("adds idempotent jobs with the queue retry and retention policy", async () => {
|
|
43
52
|
const { client, queue, factory } = await build()
|
|
44
53
|
const payload = { orderId: "order-1" }
|
|
45
54
|
|
|
46
|
-
await client.add("orders", "outbox-1", payload)
|
|
47
|
-
await client.add("orders", "outbox-2", payload)
|
|
55
|
+
await client.add("orders", "outbox-1", payload, "request-1")
|
|
56
|
+
await client.add("orders", "outbox-2", payload, "request-2")
|
|
48
57
|
|
|
49
58
|
expect(factory.queue).toHaveBeenCalledTimes(1)
|
|
50
59
|
expect(factory.queue).toHaveBeenCalledWith("orders", {
|
|
51
60
|
prefix: "spec.",
|
|
52
61
|
connection: { host: "redis.test", port: 6380, db: 7, maxRetriesPerRequest: null },
|
|
53
62
|
})
|
|
54
|
-
expect(queue.add).toHaveBeenNthCalledWith(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
expect(queue.add).toHaveBeenNthCalledWith(
|
|
64
|
+
1,
|
|
65
|
+
"orders",
|
|
66
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload, correlationId: "request-1" },
|
|
67
|
+
{
|
|
68
|
+
jobId: "outbox-1",
|
|
69
|
+
attempts: ATTEMPTS,
|
|
70
|
+
backoff: { type: "exponential", delay: BACKOFF_MS },
|
|
71
|
+
removeOnComplete: { age: KEEP_COMPLETED_SECONDS },
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("rejects unsafe correlation before writing a new job", async () => {
|
|
77
|
+
const { client, factory } = await build()
|
|
78
|
+
|
|
79
|
+
await expect(client.add("orders", "outbox-1", {}, "unsafe\nrequest")).rejects.toMatchObject({
|
|
80
|
+
code: QueueErrorCode.CorrelationInvalid,
|
|
59
81
|
})
|
|
82
|
+
expect(factory.queue).not.toHaveBeenCalled()
|
|
60
83
|
})
|
|
61
84
|
|
|
62
85
|
it("upserts schedulers with their payload or an empty payload", async () => {
|
|
@@ -69,16 +92,26 @@ describe("BullmqQueueTransportClient", () => {
|
|
|
69
92
|
1,
|
|
70
93
|
"settlement",
|
|
71
94
|
{ every: 60_000 },
|
|
72
|
-
{
|
|
95
|
+
{
|
|
96
|
+
name: "billing",
|
|
97
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: { shard: 2 } },
|
|
98
|
+
opts: { attempts: ATTEMPTS },
|
|
99
|
+
},
|
|
73
100
|
)
|
|
74
101
|
expect(queue.upsertJobScheduler).toHaveBeenNthCalledWith(
|
|
75
102
|
2,
|
|
76
103
|
"cleanup",
|
|
77
104
|
{ every: 120_000 },
|
|
78
|
-
{
|
|
105
|
+
{
|
|
106
|
+
name: "billing",
|
|
107
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {} },
|
|
108
|
+
opts: { attempts: ATTEMPTS },
|
|
109
|
+
},
|
|
79
110
|
)
|
|
80
111
|
})
|
|
112
|
+
})
|
|
81
113
|
|
|
114
|
+
describe("BullmqQueueTransportClient worker deliveries", () => {
|
|
82
115
|
it("starts a worker that translates BullMQ jobs into queue deliveries", async () => {
|
|
83
116
|
const { client, factory } = await build()
|
|
84
117
|
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
@@ -86,29 +119,181 @@ describe("BullmqQueueTransportClient", () => {
|
|
|
86
119
|
|
|
87
120
|
await client.work(handler, 3)
|
|
88
121
|
|
|
89
|
-
expect(factory.worker).toHaveBeenCalledWith("orders",
|
|
122
|
+
expect(factory.worker).toHaveBeenCalledWith("orders", expect.any(Function), {
|
|
90
123
|
prefix: "spec.",
|
|
91
124
|
connection: { host: "redis.test", port: 6380, db: 7, maxRetriesPerRequest: null },
|
|
92
125
|
concurrency: 3,
|
|
93
126
|
})
|
|
94
127
|
const processor = factory.worker.mock.calls[0]?.[1]
|
|
95
|
-
await processor?.(
|
|
96
|
-
|
|
128
|
+
await processor?.(
|
|
129
|
+
mock<BullmqJob>({
|
|
130
|
+
id: "job-1",
|
|
131
|
+
data: {
|
|
132
|
+
schema: BULLMQ_JOB_SCHEMA,
|
|
133
|
+
kind: "enqueued",
|
|
134
|
+
payload: { orderId: "order-1" },
|
|
135
|
+
correlationId: "request-1",
|
|
136
|
+
},
|
|
137
|
+
attemptsMade: 2,
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
await processor?.({
|
|
141
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: { orderId: "order-2" } },
|
|
142
|
+
attemptsMade: 0,
|
|
143
|
+
} satisfies BullmqJob)
|
|
144
|
+
await processor?.({
|
|
145
|
+
id: "tick-3",
|
|
146
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {} },
|
|
147
|
+
attemptsMade: 1,
|
|
148
|
+
} satisfies BullmqJob)
|
|
97
149
|
|
|
98
150
|
expect(handler.handle).toHaveBeenNthCalledWith(1, {
|
|
99
151
|
id: "job-1",
|
|
152
|
+
correlationId: "request-1",
|
|
100
153
|
queue: "orders",
|
|
101
154
|
payload: { orderId: "order-1" },
|
|
102
155
|
attempt: 3,
|
|
103
156
|
})
|
|
104
157
|
expect(handler.handle).toHaveBeenNthCalledWith(2, {
|
|
105
158
|
id: "",
|
|
159
|
+
correlationId: MINTED_ID,
|
|
106
160
|
queue: "orders",
|
|
107
161
|
payload: { orderId: "order-2" },
|
|
108
162
|
attempt: 1,
|
|
109
163
|
})
|
|
164
|
+
expect(handler.handle).toHaveBeenNthCalledWith(3, {
|
|
165
|
+
id: "tick-3",
|
|
166
|
+
correlationId: "tick-3",
|
|
167
|
+
queue: "orders",
|
|
168
|
+
payload: {},
|
|
169
|
+
attempt: 2,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe("BullmqQueueTransportClient stored job upgrades", () => {
|
|
175
|
+
it("preserves raw payloads and the same job correlation through legacy retries", async () => {
|
|
176
|
+
const { client, factory, ids } = await build()
|
|
177
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
178
|
+
await client.work(handler, 3)
|
|
179
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
180
|
+
const payload = { orderId: "before-upgrade", kind: "expire-order", payload: { reason: "unpaid" } }
|
|
181
|
+
|
|
182
|
+
await processor?.({ id: "legacy-job", data: payload, attemptsMade: 0 })
|
|
183
|
+
await processor?.({ id: "legacy-job", data: payload, attemptsMade: 1 })
|
|
184
|
+
|
|
185
|
+
expect(handler.handle).toHaveBeenNthCalledWith(1, {
|
|
186
|
+
id: "legacy-job",
|
|
187
|
+
queue: "orders",
|
|
188
|
+
payload,
|
|
189
|
+
correlationId: "legacy-job",
|
|
190
|
+
attempt: 1,
|
|
191
|
+
})
|
|
192
|
+
expect(handler.handle).toHaveBeenNthCalledWith(2, {
|
|
193
|
+
id: "legacy-job",
|
|
194
|
+
queue: "orders",
|
|
195
|
+
payload,
|
|
196
|
+
correlationId: "legacy-job",
|
|
197
|
+
attempt: 2,
|
|
198
|
+
})
|
|
199
|
+
expect(ids.issued).toEqual([])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it.each(["enqueued", "scheduled"])("keeps legacy business fields even when kind is %s", async (kind) => {
|
|
203
|
+
const { client, factory, ids } = await build()
|
|
204
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
205
|
+
await client.work(handler, 3)
|
|
206
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
207
|
+
const payload = {
|
|
208
|
+
schema: "order/business@1",
|
|
209
|
+
kind,
|
|
210
|
+
correlationId: "business\nkey",
|
|
211
|
+
payload: { orderId: "legacy-order" },
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await processor?.({ id: "legacy-collision", data: payload, attemptsMade: 0 })
|
|
215
|
+
|
|
216
|
+
expect(handler.handle).toHaveBeenCalledWith({
|
|
217
|
+
id: "legacy-collision",
|
|
218
|
+
queue: "orders",
|
|
219
|
+
payload,
|
|
220
|
+
correlationId: "legacy-collision",
|
|
221
|
+
attempt: 1,
|
|
222
|
+
})
|
|
223
|
+
expect(ids.issued).toEqual([])
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe("BullmqQueueTransportClient correlation roots and marked envelopes", () => {
|
|
228
|
+
it("mints only when legacy data has no job id and accepts explicit scheduled correlation", async () => {
|
|
229
|
+
const { client, factory, ids } = await build()
|
|
230
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
231
|
+
await client.work(handler, 3)
|
|
232
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
233
|
+
|
|
234
|
+
await processor?.({ data: { orderId: "legacy-no-id" }, attemptsMade: 0 })
|
|
235
|
+
await processor?.({
|
|
236
|
+
data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {}, correlationId: "upstream-tick" },
|
|
237
|
+
attemptsMade: 0,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(handler.handle.mock.calls.map(([delivery]) => delivery.correlationId)).toEqual([
|
|
241
|
+
MINTED_ID,
|
|
242
|
+
"upstream-tick",
|
|
243
|
+
])
|
|
244
|
+
expect(ids.issued).toEqual([MINTED_ID])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it.each<unknown>([
|
|
248
|
+
null,
|
|
249
|
+
{ schema: "starci/queue@2", kind: "enqueued", payload: {}, correlationId: "request-1" },
|
|
250
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "unknown", payload: {} },
|
|
251
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: {}, correlationId: undefined },
|
|
252
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: null, correlationId: "request-1" },
|
|
253
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: {}, correlationId: "bad\nidentity" },
|
|
254
|
+
{ schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {}, correlationId: undefined },
|
|
255
|
+
])("rejects malformed current data before business delivery (%j)", async (data) => {
|
|
256
|
+
const { client, factory } = await build()
|
|
257
|
+
const handler = mock<QueueHandler>({ queue: "orders" })
|
|
258
|
+
await client.work(handler, 3)
|
|
259
|
+
const processor = factory.worker.mock.calls[0]?.[1]
|
|
260
|
+
|
|
261
|
+
await expect(processor?.({ id: "stored-job", data, attemptsMade: 0 })).rejects.toMatchObject({
|
|
262
|
+
code: QueueErrorCode.MalformedMessage,
|
|
263
|
+
})
|
|
264
|
+
expect(handler.handle).not.toHaveBeenCalled()
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe("BullmqQueueTransportClient driver errors", () => {
|
|
269
|
+
it("routes queue driver errors through the logger once per queue", async () => {
|
|
270
|
+
const { client, queue, logger } = await build()
|
|
271
|
+
await client.add("orders", "outbox-1", {}, "request-1")
|
|
272
|
+
await client.add("orders", "outbox-2", {}, "request-2")
|
|
273
|
+
const cause = new Error("redis connection lost")
|
|
274
|
+
|
|
275
|
+
const listener = queue.on.mock.calls[0]?.[1]
|
|
276
|
+
listener?.(cause)
|
|
277
|
+
|
|
278
|
+
expect(queue.on).toHaveBeenCalledTimes(1)
|
|
279
|
+
expect(queue.on).toHaveBeenCalledWith("error", expect.any(Function))
|
|
280
|
+
expect(logger.error).toHaveBeenCalledWith(QueueLogEvent.QueueFailed, cause, { queue: "orders" })
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it("routes worker driver errors through the logger", async () => {
|
|
284
|
+
const { client, worker, logger } = await build()
|
|
285
|
+
await client.work(mock<QueueHandler>({ queue: "orders" }), 3)
|
|
286
|
+
const cause = new Error("blocking redis connection lost")
|
|
287
|
+
|
|
288
|
+
const listener = worker.on.mock.calls[0]?.[1]
|
|
289
|
+
listener?.(cause)
|
|
290
|
+
|
|
291
|
+
expect(worker.on).toHaveBeenCalledWith("error", expect.any(Function))
|
|
292
|
+
expect(logger.error).toHaveBeenCalledWith(QueueLogEvent.WorkerFailed, cause, { queue: "orders" })
|
|
110
293
|
})
|
|
294
|
+
})
|
|
111
295
|
|
|
296
|
+
describe("BullmqQueueTransportClient wait and shutdown", () => {
|
|
112
297
|
it("waits for the requested delay", async () => {
|
|
113
298
|
jest.useFakeTimers()
|
|
114
299
|
try {
|
|
@@ -132,7 +317,7 @@ describe("BullmqQueueTransportClient", () => {
|
|
|
132
317
|
finishWorkerClose = resolve
|
|
133
318
|
}),
|
|
134
319
|
)
|
|
135
|
-
await client.add("orders", "outbox-1", {})
|
|
320
|
+
await client.add("orders", "outbox-1", {}, "request-1")
|
|
136
321
|
await client.work(handler, 3)
|
|
137
322
|
|
|
138
323
|
const closing = client.close()
|