@cosmicdrift/kumiko-framework 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -1
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +206 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +299 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +101 -0
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +61 -2
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
|
+
import { prepareJobRun } from "./job-runner-context.js";
|
|
3
|
+
import { attemptNumberOf, buildRetryBullOpts, DEFER_COUNT_KEY, deferDelayMs, isFinalAttempt, priorAttemptsOf, priorDeferCountOf, } from "./job-runner-helpers.js";
|
|
4
|
+
import { ACTIVE_TENANT_IDS_QUERY_NAME, perTenantChildJobId } from "./job-runner-schedulers.js";
|
|
5
|
+
import { announceDropped, announceQueued, isOverPerTenantLimit, laneForJob, logFailedAttempt, } from "./job-runner-scope.js";
|
|
6
|
+
import { resolveMaxPerTenant } from "./max-per-tenant.js";
|
|
7
|
+
// Default lock-TTL for sequential jobs that didn't declare a timeout.
|
|
8
|
+
// 5 minutes matches BullMQ's default stalledInterval — long enough for
|
|
9
|
+
// any reasonable handler, short enough that a crashed worker recovers
|
|
10
|
+
// without manual intervention.
|
|
11
|
+
const SEQUENTIAL_DEFAULT_TTL_SEC = 305;
|
|
12
|
+
// How long to wait before re-trying a busy sequential lock. Short enough
|
|
13
|
+
// to feel responsive, long enough that we don't hammer Redis.
|
|
14
|
+
const SEQUENTIAL_RETRY_DELAY_MS = 200;
|
|
15
|
+
export const PER_TENANT_PREFIX = "_perTenant:";
|
|
16
|
+
export function perTenantTargetOf(jobName) {
|
|
17
|
+
return jobName.startsWith(PER_TENANT_PREFIX)
|
|
18
|
+
? jobName.slice(PER_TENANT_PREFIX.length)
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
function missingTenantResolverError(scope, actualName) {
|
|
22
|
+
if (scope.registry.getQueryHandler(ACTIVE_TENANT_IDS_QUERY_NAME)) {
|
|
23
|
+
return new Error(`perTenant job "${actualName}": the tenant feature is mounted but the job runner has no ` +
|
|
24
|
+
"`context.db`: pass a DbConnection in the runner context or supply options.getActiveTenantIds");
|
|
25
|
+
}
|
|
26
|
+
return new Error(`perTenant job "${actualName}" requires either options.getActiveTenantIds or the ` +
|
|
27
|
+
`tenant feature mounted (it registers "${ACTIVE_TENANT_IDS_QUERY_NAME}", which the ` +
|
|
28
|
+
"framework uses to resolve active tenants on its own)");
|
|
29
|
+
}
|
|
30
|
+
async function enqueuePerTenantChildren(scope, bullJob, actualName) {
|
|
31
|
+
const { getActiveTenantIds } = scope;
|
|
32
|
+
if (!getActiveTenantIds) {
|
|
33
|
+
throw missingTenantResolverError(scope, actualName);
|
|
34
|
+
}
|
|
35
|
+
const actualDef = scope.allJobs.get(actualName);
|
|
36
|
+
if (!actualDef) {
|
|
37
|
+
throw new Error(`Unknown job: ${actualName}`);
|
|
38
|
+
}
|
|
39
|
+
const tenantIds = await getActiveTenantIds();
|
|
40
|
+
const targetQueue = scope.queues[laneForJob(actualDef)];
|
|
41
|
+
// wrapperJobId is only absent for a Job BullMQ somehow never assigned
|
|
42
|
+
// an id to (not observed in practice) — falling back to a constant
|
|
43
|
+
// there would collide every such run's children onto the same ids
|
|
44
|
+
// and silently swallow later runs as "duplicates", which is worse
|
|
45
|
+
// than the duplicate this is meant to prevent. Skip id-based dedup
|
|
46
|
+
// for that edge case instead: enqueue plainly, same as before.
|
|
47
|
+
const wrapperJobId = bullJob.id;
|
|
48
|
+
for (const tenantId of tenantIds) {
|
|
49
|
+
const childData = { ...bullJob.data, _tenantId: tenantId };
|
|
50
|
+
const child = await targetQueue.add(actualName, childData, {
|
|
51
|
+
...buildRetryBullOpts(actualDef),
|
|
52
|
+
// Dedup over wrapper retries only holds as long as the children
|
|
53
|
+
// stay in Redis for the wrapper's whole retry window — see the
|
|
54
|
+
// COMPLETED_JOB_RETENTION_AGE_SEC invariant check in createJobRunner.
|
|
55
|
+
...(wrapperJobId !== undefined ? { jobId: perTenantChildJobId(wrapperJobId, tenantId) } : {}),
|
|
56
|
+
});
|
|
57
|
+
await announceQueued(scope, actualName, actualDef, child, childData);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Handle perTenant dispatch jobs — fan out to one job per tenant. The
|
|
61
|
+
// fan-out re-enqueues into the lane the actual job is assigned to;
|
|
62
|
+
// the _perTenant wrapper itself always lives in the consumer-lane
|
|
63
|
+
// (it's picked up by this runner's own worker).
|
|
64
|
+
export async function fanOutPerTenantJob(scope, bullJob, actualName) {
|
|
65
|
+
// These throws happen before runInSpan, so its onJobFailed never sees them
|
|
66
|
+
// and a drained test stack would stay green over a fan-out that never ran.
|
|
67
|
+
try {
|
|
68
|
+
await enqueuePerTenantChildren(scope, bullJob, actualName);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// A retry may still succeed, and a succeeding wrapper reports no completion
|
|
72
|
+
// that could clear an earlier failure, so only the final attempt is reported.
|
|
73
|
+
const retries = scope.allJobs.get(actualName)?.retries ?? 0;
|
|
74
|
+
const finalAttempt = bullJob.attemptsMade + 1 >= retries + 1;
|
|
75
|
+
logFailedAttempt(scope, {
|
|
76
|
+
job: bullJob.name,
|
|
77
|
+
jobId: bullJob.id ?? "unknown",
|
|
78
|
+
attempt: bullJob.attemptsMade + 1,
|
|
79
|
+
finalAttempt,
|
|
80
|
+
err,
|
|
81
|
+
});
|
|
82
|
+
if (finalAttempt) {
|
|
83
|
+
await reportPreRunFailure(scope, bullJob.name, bullJob, err);
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function reportPreRunFailure(scope, jobName, bullJob, err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
await scope.options.onJobFailed?.(jobName, bullJob.id ?? "unknown", message, [
|
|
91
|
+
{ level: "error", message, timestamp: Temporal.Now.instant() },
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
// prepareJobRun and onJobStart throw before the handler span exists, so the
|
|
95
|
+
// span's own failure recording never sees them.
|
|
96
|
+
export async function startJobRun(scope, jobDef, bullJob, token) {
|
|
97
|
+
try {
|
|
98
|
+
const run = prepareJobRun(scope, jobDef, bullJob, token);
|
|
99
|
+
await scope.options.onJobStart?.(run.jobName, run.jobId, run.meta);
|
|
100
|
+
return run;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const finalAttempt = isFinalAttempt(bullJob, jobDef);
|
|
104
|
+
logFailedAttempt(scope, {
|
|
105
|
+
job: bullJob.name,
|
|
106
|
+
jobId: bullJob.id ?? "unknown",
|
|
107
|
+
attempt: attemptNumberOf(bullJob),
|
|
108
|
+
finalAttempt,
|
|
109
|
+
err,
|
|
110
|
+
});
|
|
111
|
+
if (finalAttempt) {
|
|
112
|
+
await reportPreRunFailure(scope, bullJob.name, bullJob, err);
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function resolveJobDefinitionOrFail(scope, bullJob) {
|
|
118
|
+
const jobName = bullJob.name;
|
|
119
|
+
const jobDef = scope.allJobs.get(jobName);
|
|
120
|
+
if (jobDef)
|
|
121
|
+
return jobDef;
|
|
122
|
+
const err = new Error(`Unknown job: ${jobName}`);
|
|
123
|
+
// Final regardless of attempts left: a retry cannot make the job exist.
|
|
124
|
+
logFailedAttempt(scope, {
|
|
125
|
+
job: jobName,
|
|
126
|
+
jobId: bullJob.id ?? "unknown",
|
|
127
|
+
attempt: bullJob.attemptsMade + 1,
|
|
128
|
+
finalAttempt: true,
|
|
129
|
+
err,
|
|
130
|
+
});
|
|
131
|
+
await reportPreRunFailure(scope, jobName, bullJob, err);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
// maxPerTenant defer: checked before anything is recorded for this run, so a
|
|
135
|
+
// deferred pickup leaves no span, run-log entry or failure trace and does not
|
|
136
|
+
// consume an attempt (moveToDelayed + DelayedError, not a retry).
|
|
137
|
+
export async function deferOverTenantLimit(scope, jobDef, bullJob, token) {
|
|
138
|
+
const maxPerTenant = resolveMaxPerTenant(jobDef);
|
|
139
|
+
const deferTenantId = bullJob.data["_tenantId"];
|
|
140
|
+
if (maxPerTenant?.onLimit === "defer" &&
|
|
141
|
+
typeof deferTenantId === "string" &&
|
|
142
|
+
token !== undefined &&
|
|
143
|
+
bullJob.moveToDelayed &&
|
|
144
|
+
(await isOverPerTenantLimit(scope, bullJob.name, deferTenantId, maxPerTenant.limit, {
|
|
145
|
+
activeOnly: true,
|
|
146
|
+
self: { id: bullJob.id, processedOn: bullJob.processedOn },
|
|
147
|
+
}))) {
|
|
148
|
+
const deferCount = priorDeferCountOf(bullJob.data, DEFER_COUNT_KEY);
|
|
149
|
+
await bullJob.updateData?.({ ...bullJob.data, [DEFER_COUNT_KEY]: deferCount + 1 });
|
|
150
|
+
await bullJob.moveToDelayed(Date.now() + deferDelayMs(maxPerTenant.deferMs, deferCount), token);
|
|
151
|
+
return "deferred";
|
|
152
|
+
}
|
|
153
|
+
return "run";
|
|
154
|
+
}
|
|
155
|
+
async function requeueBehindSequentialLock(scope, jobDef, bullJob) {
|
|
156
|
+
// Re-enqueue onto the job's own lane-queue. In practice that's the
|
|
157
|
+
// same queue the worker just picked from (since only the consuming
|
|
158
|
+
// lane runs handleJob at all), but route explicitly — no implicit
|
|
159
|
+
// coupling to "whichever queue the caller happened to be on".
|
|
160
|
+
// Attempts already spent travel in _priorAttempts so a lock conflict
|
|
161
|
+
// on a retry cannot hand out a fresh budget and exceed `retries`.
|
|
162
|
+
const priorAttempts = priorAttemptsOf(bullJob.data) + bullJob.attemptsMade;
|
|
163
|
+
const requeuedData = { ...bullJob.data, _priorAttempts: priorAttempts };
|
|
164
|
+
const requeued = await scope.queues[laneForJob(jobDef)].add(bullJob.name, requeuedData, {
|
|
165
|
+
delay: SEQUENTIAL_RETRY_DELAY_MS,
|
|
166
|
+
...buildRetryBullOpts(jobDef, priorAttempts),
|
|
167
|
+
});
|
|
168
|
+
if (bullJob.id !== undefined && bullJob.id !== requeued.id) {
|
|
169
|
+
await announceDropped(scope, bullJob.name, jobDef, bullJob.id, bullJob.data);
|
|
170
|
+
}
|
|
171
|
+
await announceQueued(scope, bullJob.name, jobDef, requeued, requeuedData);
|
|
172
|
+
}
|
|
173
|
+
// Sequential gate: try to claim the per-name lock. If another worker
|
|
174
|
+
// (or this worker on a different bullJob) holds it, re-enqueue with a
|
|
175
|
+
// small delay and exit *successfully* — using throw would burn the
|
|
176
|
+
// job's retry budget and pollute failure metrics, but a re-enqueue
|
|
177
|
+
// looks like an ordinary handoff to BullMQ.
|
|
178
|
+
export async function claimSequentialSlot(scope, jobDef, bullJob) {
|
|
179
|
+
const { sequentialLock } = scope;
|
|
180
|
+
if (jobDef.concurrency !== "sequential" || !sequentialLock) {
|
|
181
|
+
return { kind: "run", lockToken: null };
|
|
182
|
+
}
|
|
183
|
+
const ttlSec = jobDef.timeout ? Math.ceil(jobDef.timeout / 1000) + 5 : SEQUENTIAL_DEFAULT_TTL_SEC;
|
|
184
|
+
const lockToken = await sequentialLock.acquire(bullJob.name, { ttlSeconds: ttlSec });
|
|
185
|
+
if (lockToken)
|
|
186
|
+
return { kind: "run", lockToken };
|
|
187
|
+
await requeueBehindSequentialLock(scope, jobDef, bullJob);
|
|
188
|
+
return { kind: "requeued" };
|
|
189
|
+
}
|
|
190
|
+
// Release the sequential lock value-matched (Lua compare-and-delete
|
|
191
|
+
// inside DistributedLock). A TTL-expired lock that's been claimed by
|
|
192
|
+
// a different owner stays put — releasing it would break sequencing
|
|
193
|
+
// for the new owner.
|
|
194
|
+
export async function releaseSequentialSlot(scope, jobName, lockToken) {
|
|
195
|
+
if (lockToken && scope.sequentialLock) {
|
|
196
|
+
await scope.sequentialLock.release(jobName, lockToken);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -3,18 +3,20 @@ import type { ConfigAccessor } from "@cosmicdrift/kumiko-types/config";
|
|
|
3
3
|
import type { TzContext } from "@cosmicdrift/kumiko-types/tz-context";
|
|
4
4
|
import type { JobsOptions, Queue } from "bullmq";
|
|
5
5
|
import type { TenantDb } from "../db/tenant-db.ts";
|
|
6
|
-
import type { AppContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
|
|
6
|
+
import type { AppContext, JobDeferral, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
|
|
7
7
|
import type { Logger } from "../logging/types.ts";
|
|
8
8
|
import type { Meter, SerializedTraceContext, Tracer } from "../observability/index.ts";
|
|
9
|
-
import type { JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
|
|
9
|
+
import type { JobInvocation, JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
|
|
10
10
|
export declare function isJobTriggerEvent(value: unknown): value is JobTriggerEvent;
|
|
11
11
|
export declare function stampGatedWriteOrigin(data: Record<string, unknown>): void;
|
|
12
12
|
export declare function createJobLogger(logs: JobLogEntry[], baseContext?: Record<string, unknown>): Logger;
|
|
13
13
|
export declare function serializeJobSubject(subject: Readonly<Record<string, JobSubjectValue>>): string;
|
|
14
14
|
export declare function jobSubjectKey(jobName: string, payload: Record<string, unknown>, fields: readonly string[] | undefined, declaration: "tenantVisibleFailure" | "tenantVisibleRun"): string | null;
|
|
15
|
-
export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">): Promise<TzContext>;
|
|
15
|
+
export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">, logger: Pick<Logger, "warn">): Promise<TzContext>;
|
|
16
16
|
export declare function missingDbProxy(jobName: string): TenantDb;
|
|
17
17
|
export declare function priorAttemptsOf(data: Record<string, unknown>): number;
|
|
18
|
+
export declare function attemptNumberOf(bullJob: JobInvocation): number;
|
|
19
|
+
export declare function isFinalAttempt(bullJob: JobInvocation, jobDef: JobDefinition): boolean;
|
|
18
20
|
export declare function tenantFailureMessageKey(err: unknown, declaration: JobDefinition["tenantVisibleFailure"]): string | null;
|
|
19
21
|
export declare const TRACE_CONTEXT_KEY = "_traceContext";
|
|
20
22
|
export declare const DEFER_COUNT_KEY = "_deferCount";
|
|
@@ -42,3 +44,15 @@ export declare function payloadMatchesTriggerFilter(where: Readonly<Record<strin
|
|
|
42
44
|
export declare const COMPLETED_JOB_RETENTION_AGE_SEC = 86400;
|
|
43
45
|
export declare const FAILED_JOB_RETENTION_AGE_SEC = 604800;
|
|
44
46
|
export declare function maxRetryWindowMs(jobDef: JobDefinition): number;
|
|
47
|
+
export declare function isJobDeferral(value: unknown): value is JobDeferral;
|
|
48
|
+
export declare function priorDeferCountOf(data: Record<string, unknown>, key: string): number;
|
|
49
|
+
export declare function handlerPayloadOf(data: Record<string, unknown>): Record<string, unknown>;
|
|
50
|
+
export declare function tenantIdOfJobData(data: Record<string, unknown>): string;
|
|
51
|
+
export declare function deferDelayMs(deferMs: number, deferCount: number): number;
|
|
52
|
+
export declare function isActiveBefore(other: {
|
|
53
|
+
readonly id?: string | undefined;
|
|
54
|
+
readonly processedOn?: number | undefined;
|
|
55
|
+
}, self: {
|
|
56
|
+
readonly id: string | undefined;
|
|
57
|
+
readonly processedOn: number | undefined;
|
|
58
|
+
}): boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
3
|
import { requestContext } from "../api/request-context.js";
|
|
4
|
+
import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
|
|
4
5
|
import { InternalError } from "../errors/index.js";
|
|
5
6
|
import { isKumikoError } from "../errors/kumiko-error.js";
|
|
6
7
|
import { createFallbackLogger, scrubLogText } from "../logging/index.js";
|
|
@@ -89,8 +90,22 @@ export function jobSubjectKey(jobName, payload, fields, declaration) {
|
|
|
89
90
|
}
|
|
90
91
|
return serializeJobSubject(subject);
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
// A job that never reads ctx.tz must not fail because the tenant config read did.
|
|
94
|
+
async function readTenantTimezoneOrUndefined(config, logger) {
|
|
95
|
+
if (!config)
|
|
96
|
+
return undefined;
|
|
97
|
+
try {
|
|
98
|
+
return await config(TENANT_TIMEZONE_CONFIG_KEY);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
logger.warn("tenant timezone config read failed, falling back to UTC", {
|
|
102
|
+
error: scrubLogText(err instanceof Error ? err.message : String(err)),
|
|
103
|
+
});
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function resolveJobTz(config, context, logger) {
|
|
108
|
+
const tenantTz = await readTenantTimezoneOrUndefined(config, logger);
|
|
94
109
|
return createTzContext({
|
|
95
110
|
...(context.geoTzProvider !== undefined && { geoTz: context.geoTzProvider }),
|
|
96
111
|
...(context.clock !== undefined && { clock: context.clock }),
|
|
@@ -113,6 +128,14 @@ export function priorAttemptsOf(data) {
|
|
|
113
128
|
const value = data["_priorAttempts"];
|
|
114
129
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
|
|
115
130
|
}
|
|
131
|
+
export function attemptNumberOf(bullJob) {
|
|
132
|
+
return bullJob.attemptsMade + 1 + priorAttemptsOf(bullJob.data);
|
|
133
|
+
}
|
|
134
|
+
// BullMQ stops retrying once attemptsMade reaches the configured attempts
|
|
135
|
+
// (`retries + 1`), so this is the attempt whose failure is final.
|
|
136
|
+
export function isFinalAttempt(bullJob, jobDef) {
|
|
137
|
+
return bullJob.retryable === false || attemptNumberOf(bullJob) >= (jobDef.retries ?? 0) + 1;
|
|
138
|
+
}
|
|
116
139
|
// Only a translation key ever travels to the tenant: the thrown error's own
|
|
117
140
|
// i18nKey when it carries one, otherwise the key declared at the job. The
|
|
118
141
|
// error message itself can echo provider or payload content and stays on the
|
|
@@ -269,3 +292,39 @@ export function maxRetryWindowMs(jobDef) {
|
|
|
269
292
|
const type = typeof jobDef.backoff === "string" ? jobDef.backoff : jobDef.backoff.type;
|
|
270
293
|
return type === "exponential" ? delayMs * (2 ** jobDef.retries - 1) : jobDef.retries * delayMs;
|
|
271
294
|
}
|
|
295
|
+
export function isJobDeferral(value) {
|
|
296
|
+
return (typeof value === "object" && value !== null && "kind" in value && value.kind === "job-deferral");
|
|
297
|
+
}
|
|
298
|
+
export function priorDeferCountOf(data, key) {
|
|
299
|
+
const prior = data[key];
|
|
300
|
+
return typeof prior === "number" ? prior : 0;
|
|
301
|
+
}
|
|
302
|
+
export function handlerPayloadOf(data) {
|
|
303
|
+
const payload = {};
|
|
304
|
+
for (const [k, v] of Object.entries(data)) {
|
|
305
|
+
if (!k.startsWith("_"))
|
|
306
|
+
payload[k] = v;
|
|
307
|
+
}
|
|
308
|
+
return payload;
|
|
309
|
+
}
|
|
310
|
+
export function tenantIdOfJobData(data) {
|
|
311
|
+
return (data["_tenantId"] ?? // @cast-boundary dynamic-key
|
|
312
|
+
data["tenantId"] ?? // @cast-boundary dynamic-key
|
|
313
|
+
SYSTEM_TENANT_ID);
|
|
314
|
+
}
|
|
315
|
+
// Growing delay bounds Redis and worker load while one tenant has a large
|
|
316
|
+
// backlog: every deferred job would otherwise be re-checked every deferMs.
|
|
317
|
+
export function deferDelayMs(deferMs, deferCount) {
|
|
318
|
+
const grown = Math.min(deferMs * 2 ** Math.min(deferCount, DEFER_BACKOFF_MAX_DOUBLINGS), DEFER_DELAY_CAP_MS);
|
|
319
|
+
return grown + Math.floor(Math.random() * deferMs);
|
|
320
|
+
}
|
|
321
|
+
export function isActiveBefore(other, self) {
|
|
322
|
+
if (other.id === self.id)
|
|
323
|
+
return false;
|
|
324
|
+
if (self.processedOn === undefined)
|
|
325
|
+
return true;
|
|
326
|
+
const otherProcessedOn = other.processedOn ?? 0;
|
|
327
|
+
if (otherProcessedOn !== self.processedOn)
|
|
328
|
+
return otherProcessedOn < self.processedOn;
|
|
329
|
+
return (other.id ?? "") < (self.id ?? "");
|
|
330
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Queue } from "bullmq";
|
|
2
|
+
import type { AppContext, DispatchWriteRef, JobDefinition, JobRunIn, Registry, TenantId } from "../engine/types/index.ts";
|
|
3
|
+
import type { createFallbackLogger } from "../logging/index.ts";
|
|
4
|
+
import type { EscapeHatchProcessDedup, EscapeHatchReportWindow, Tracer } from "../observability/index.ts";
|
|
5
|
+
import type { DistributedLock } from "../pipeline/distributed-lock.ts";
|
|
6
|
+
import type { JobRunner, JobRunnerOptions } from "./job-runner-types.ts";
|
|
7
|
+
export type JobRunnerScope = {
|
|
8
|
+
readonly registry: Registry;
|
|
9
|
+
readonly context: AppContext;
|
|
10
|
+
readonly options: JobRunnerOptions;
|
|
11
|
+
readonly allJobs: ReadonlyMap<string, JobDefinition>;
|
|
12
|
+
readonly queues: Readonly<Record<JobRunIn, Queue>>;
|
|
13
|
+
readonly consumerLane: JobRunIn | undefined;
|
|
14
|
+
readonly tracer: Tracer;
|
|
15
|
+
readonly errorLogger: ReturnType<typeof createFallbackLogger>;
|
|
16
|
+
readonly sequentialLock: DistributedLock | null;
|
|
17
|
+
readonly getActiveTenantIds: (() => Promise<TenantId[]>) | undefined;
|
|
18
|
+
readonly escapeHatchReportWindow: EscapeHatchReportWindow;
|
|
19
|
+
readonly systemCronEscapeHatchDedup: EscapeHatchProcessDedup;
|
|
20
|
+
readonly getSelfRunner: () => JobRunner | undefined;
|
|
21
|
+
readonly getDispatchWriteRef: () => DispatchWriteRef | undefined;
|
|
22
|
+
};
|
|
23
|
+
export declare function laneForJob(def: {
|
|
24
|
+
readonly runIn?: JobRunIn | undefined;
|
|
25
|
+
}): JobRunIn;
|
|
26
|
+
export declare function isOverPerTenantLimit(scope: JobRunnerScope, jobName: string, tenantId: string, max: number, countFilter?: {
|
|
27
|
+
readonly activeOnly?: boolean;
|
|
28
|
+
readonly self?: {
|
|
29
|
+
readonly id: string | undefined;
|
|
30
|
+
readonly processedOn: number | undefined;
|
|
31
|
+
};
|
|
32
|
+
}): Promise<boolean>;
|
|
33
|
+
export declare function announceQueued(scope: JobRunnerScope, jobName: string, jobDef: JobDefinition, queuedJob: {
|
|
34
|
+
readonly id?: string | undefined;
|
|
35
|
+
readonly timestamp: number;
|
|
36
|
+
}, data: Record<string, unknown>): Promise<void>;
|
|
37
|
+
export declare function announceDropped(scope: JobRunnerScope, jobName: string, jobDef: JobDefinition, bullJobId: string, data: Record<string, unknown>): Promise<void>;
|
|
38
|
+
export declare function logFailedAttempt(scope: JobRunnerScope, entry: {
|
|
39
|
+
readonly job: string;
|
|
40
|
+
readonly jobId: string;
|
|
41
|
+
readonly attempt: number;
|
|
42
|
+
readonly finalAttempt: boolean;
|
|
43
|
+
readonly err: unknown;
|
|
44
|
+
}): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { UnrecoverableError } from "bullmq";
|
|
2
|
+
import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
|
|
3
|
+
import { scrubLogText } from "../logging/index.js";
|
|
4
|
+
import { handlerPayloadOf, isActiveBefore, jobSubjectKey, tenantIdOfJobData, } from "./job-runner-helpers.js";
|
|
5
|
+
// Resolve the lane for a job — "worker" is the default because that's the
|
|
6
|
+
// sensible prod lane (heavy async off the request path). Jobs that opted
|
|
7
|
+
// into "api" must have been validated at registry boot already.
|
|
8
|
+
export function laneForJob(def) {
|
|
9
|
+
return def.runIn ?? "worker";
|
|
10
|
+
}
|
|
11
|
+
// Counts active + waiting jobs with this name for this tenant across
|
|
12
|
+
// BOTH lane queues. Jobs with the same name should only live in one
|
|
13
|
+
// lane (jobDef.runIn is static), but walking both is cheap and avoids
|
|
14
|
+
// a subtle bug if someone ever reassigns a job to a different lane
|
|
15
|
+
// between deploys while old queue contents are still draining.
|
|
16
|
+
// One getJobs per queue reads the requested lists in a single Lua script, so a job
|
|
17
|
+
// moving from waiting to active between two separate reads can no longer vanish
|
|
18
|
+
// from the count; that gap let a third dispatch pass the cap in CI.
|
|
19
|
+
// `activeOnly` + `self` serve the defer check, which runs inside the job being
|
|
20
|
+
// counted: it must not see waiting jobs or itself, and only counts jobs that
|
|
21
|
+
// became active before it. Without that order, workers that fetch a burst of
|
|
22
|
+
// jobs together all see each other as active and all defer, again and again.
|
|
23
|
+
export async function isOverPerTenantLimit(scope, jobName, tenantId, max, countFilter = {}) {
|
|
24
|
+
const { queues } = scope;
|
|
25
|
+
const types = countFilter.activeOnly ? ["active"] : ["active", "waiting"];
|
|
26
|
+
const results = await Promise.all([queues.api.getJobs(types), queues.worker.getJobs(types)]);
|
|
27
|
+
let count = 0;
|
|
28
|
+
for (const list of results) {
|
|
29
|
+
for (const j of list) {
|
|
30
|
+
if (j === undefined || j.name !== jobName)
|
|
31
|
+
continue;
|
|
32
|
+
if (countFilter.self && !isActiveBefore(j, countFilter.self))
|
|
33
|
+
continue;
|
|
34
|
+
const t = j.data?._tenantId; // @cast-boundary dynamic-key
|
|
35
|
+
if (t === tenantId) {
|
|
36
|
+
count += 1;
|
|
37
|
+
if (count >= max)
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Tenant-visible queued state. A callback or subject problem must never fail
|
|
45
|
+
// the enqueue; a bad subject config surfaces as a failed run instead.
|
|
46
|
+
export async function announceQueued(scope, jobName, jobDef, queuedJob, data) {
|
|
47
|
+
const { options } = scope;
|
|
48
|
+
const bullJobId = queuedJob.id;
|
|
49
|
+
const declaration = jobDef.tenantVisibleRun;
|
|
50
|
+
// skip: job did not opt in, no logger wired, or BullMQ returned no id
|
|
51
|
+
if (!declaration || !options.onJobQueued || bullJobId === undefined)
|
|
52
|
+
return;
|
|
53
|
+
const tenantId = tenantIdOfJobData(data);
|
|
54
|
+
// skip: tenant-less run — no tenant-scoped query could ever read it
|
|
55
|
+
if (tenantId === SYSTEM_TENANT_ID)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
const subject = jobSubjectKey(jobName, handlerPayloadOf(data), declaration.subjectFields, "tenantVisibleRun");
|
|
59
|
+
await options.onJobQueued(jobName, bullJobId, {
|
|
60
|
+
tenantId,
|
|
61
|
+
subject,
|
|
62
|
+
jobCreatedAtMs: queuedJob.timestamp,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
scope.errorLogger.warn("tenant-visible queued state not recorded", {
|
|
67
|
+
job: jobName,
|
|
68
|
+
error: err instanceof Error ? err.message : String(err),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function announceDropped(scope, jobName, jobDef, bullJobId, data) {
|
|
73
|
+
const { options } = scope;
|
|
74
|
+
// skip: job did not opt in or no logger wired
|
|
75
|
+
if (!jobDef.tenantVisibleRun || !options.onJobDropped)
|
|
76
|
+
return;
|
|
77
|
+
const tenantId = tenantIdOfJobData(data);
|
|
78
|
+
// skip: tenant-less run — announceQueued never recorded a row for it
|
|
79
|
+
if (tenantId === SYSTEM_TENANT_ID)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
await options.onJobDropped(jobName, bullJobId, { tenantId });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
scope.errorLogger.warn("tenant-visible queued state not cleared", {
|
|
86
|
+
job: jobName,
|
|
87
|
+
error: err instanceof Error ? err.message : String(err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Only err.message, never payload or the logs array: job payloads can carry PII.
|
|
92
|
+
export function logFailedAttempt(scope, entry) {
|
|
93
|
+
scope.errorLogger.error("job failed", {
|
|
94
|
+
job: entry.job,
|
|
95
|
+
jobId: entry.jobId,
|
|
96
|
+
attempt: entry.attempt,
|
|
97
|
+
// BullMQ never retries an UnrecoverableError, whatever attempts remain.
|
|
98
|
+
final: entry.finalAttempt || entry.err instanceof UnrecoverableError,
|
|
99
|
+
error: scrubLogText(entry.err instanceof Error ? entry.err.message : String(entry.err)),
|
|
100
|
+
...(entry.err instanceof Error && entry.err.stack && { stack: scrubLogText(entry.err.stack) }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -19,6 +19,7 @@ export type JobMeta = {
|
|
|
19
19
|
export type JobQueuedMeta = {
|
|
20
20
|
readonly tenantId: string;
|
|
21
21
|
readonly subject: string | null;
|
|
22
|
+
readonly jobCreatedAtMs: number;
|
|
22
23
|
};
|
|
23
24
|
export type JobDroppedMeta = {
|
|
24
25
|
readonly tenantId: string;
|
|
@@ -63,3 +64,14 @@ export type JobRunnerOptions = {
|
|
|
63
64
|
onJobQueued?: (jobName: string, jobId: string, meta: JobQueuedMeta) => void;
|
|
64
65
|
onJobDropped?: (jobName: string, jobId: string, meta: JobDroppedMeta) => void;
|
|
65
66
|
};
|
|
67
|
+
export type JobInvocation = {
|
|
68
|
+
readonly id?: string | undefined;
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly data: Record<string, unknown>;
|
|
71
|
+
readonly attemptsMade: number;
|
|
72
|
+
/** False for inline invocations (boot gates): nothing retries a throw. */
|
|
73
|
+
readonly retryable?: boolean;
|
|
74
|
+
readonly processedOn?: number | undefined;
|
|
75
|
+
moveToDelayed?(timestamp: number, token?: string): Promise<void>;
|
|
76
|
+
updateData?(data: Record<string, unknown>): Promise<void>;
|
|
77
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
1
|
import type { JobRunner, JobRunnerOptions } from "./job-runner-types.ts";
|
|
3
2
|
export { serializeJobSubject } from "./job-runner-helpers.ts";
|
|
4
3
|
export { bootJobIdForJobName, ORPHAN_SCHEDULER_GRACE_MS, schedulerIdForJobName, } from "./job-runner-schedulers.ts";
|