@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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 +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- 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/legal-pages.md +1 -1
- 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/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- 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 +203 -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 +293 -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 +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- 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.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +3 -3
- 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 +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- 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.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -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 +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +97 -105
- package/dist/db/migrate-runner.js +29 -26
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +137 -190
- 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.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +258 -187
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- 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 +228 -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 +52 -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 +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +130 -170
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +21 -13
- package/dist/engine/define-handler.js +13 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +13 -13
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +9 -28
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +29 -29
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +22 -13
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.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 +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- 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 +57 -669
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- 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 +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -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/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- 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 +15 -13
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- 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 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- 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 +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -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";
|
|
@@ -66,6 +67,12 @@ export function serializeJobSubject(subject) {
|
|
|
66
67
|
.sort()
|
|
67
68
|
.map((field) => [field, subject[field] ?? null]));
|
|
68
69
|
}
|
|
70
|
+
function isJobSubjectValue(value) {
|
|
71
|
+
return (value === null ||
|
|
72
|
+
typeof value === "string" ||
|
|
73
|
+
typeof value === "number" ||
|
|
74
|
+
typeof value === "boolean");
|
|
75
|
+
}
|
|
69
76
|
// Stable identity for the declared payload fields: sorted keys, so two runs
|
|
70
77
|
// with the same subject values produce the same string and the later one
|
|
71
78
|
// replaces the earlier record. Non-primitives would serialize into something
|
|
@@ -76,15 +83,29 @@ export function jobSubjectKey(jobName, payload, fields, declaration) {
|
|
|
76
83
|
const subject = {};
|
|
77
84
|
for (const field of fields) {
|
|
78
85
|
const value = payload[field] ?? null;
|
|
79
|
-
if (value
|
|
86
|
+
if (!isJobSubjectValue(value)) {
|
|
80
87
|
throw new Error(`Job "${jobName}": ${declaration}.subjectFields["${field}"] must be a primitive, got ${typeof value}`);
|
|
81
88
|
}
|
|
82
89
|
subject[field] = value;
|
|
83
90
|
}
|
|
84
91
|
return serializeJobSubject(subject);
|
|
85
92
|
}
|
|
86
|
-
|
|
87
|
-
|
|
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);
|
|
88
109
|
return createTzContext({
|
|
89
110
|
...(context.geoTzProvider !== undefined && { geoTz: context.geoTzProvider }),
|
|
90
111
|
...(context.clock !== undefined && { clock: context.clock }),
|
|
@@ -107,6 +128,14 @@ export function priorAttemptsOf(data) {
|
|
|
107
128
|
const value = data["_priorAttempts"];
|
|
108
129
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
|
|
109
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
|
+
}
|
|
110
139
|
// Only a translation key ever travels to the tenant: the thrown error's own
|
|
111
140
|
// i18nKey when it carries one, otherwise the key declared at the job. The
|
|
112
141
|
// error message itself can echo provider or payload content and stays on the
|
|
@@ -250,7 +279,7 @@ export function payloadMatchesTriggerFilter(where, payload) {
|
|
|
250
279
|
// on the children still existing in Redis when a wrapper retry re-derives
|
|
251
280
|
// their ids. The margin here is generous — queue wait time isn't bounded by
|
|
252
281
|
// backoff — to cover realistic windows. Audit trail lives in read_job_runs,
|
|
253
|
-
// not BullMQ
|
|
282
|
+
// not BullMQ.
|
|
254
283
|
export const COMPLETED_JOB_RETENTION_AGE_SEC = 86_400; // 24h
|
|
255
284
|
export const FAILED_JOB_RETENTION_AGE_SEC = 604_800; // 7d
|
|
256
285
|
// Total wall-clock time BullMQ can hold a perTenant wrapper across all its
|
|
@@ -263,3 +292,39 @@ export function maxRetryWindowMs(jobDef) {
|
|
|
263
292
|
const type = typeof jobDef.backoff === "string" ? jobDef.backoff : jobDef.backoff.type;
|
|
264
293
|
return type === "exponential" ? delayMs * (2 ** jobDef.retries - 1) : jobDef.retries * delayMs;
|
|
265
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
|
+
}
|
|
@@ -7,7 +7,7 @@ export declare const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-
|
|
|
7
7
|
* BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
|
|
8
8
|
* scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
|
|
9
9
|
* treated as old repeatables — spawning a new scheduler entry every tick and
|
|
10
|
-
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
10
|
+
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
11
11
|
* publicstatus Redis OOM). Strip `.` and `:` from the job QN.
|
|
12
12
|
*/
|
|
13
13
|
export declare function schedulerIdForJobName(jobName: string): string;
|
|
@@ -19,13 +19,13 @@ export const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-ids";
|
|
|
19
19
|
* BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
|
|
20
20
|
* scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
|
|
21
21
|
* treated as old repeatables — spawning a new scheduler entry every tick and
|
|
22
|
-
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
22
|
+
* leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
|
|
23
23
|
* publicstatus Redis OOM). Strip `.` and `:` from the job QN.
|
|
24
24
|
*/
|
|
25
25
|
export function schedulerIdForJobName(jobName) {
|
|
26
26
|
return `scheduler-${jobName.replace(/[.:]/g, "-")}`;
|
|
27
27
|
}
|
|
28
|
-
// Same colon-in-BullMQ-id hazard as schedulerIdForJobName
|
|
28
|
+
// Same colon-in-BullMQ-id hazard as schedulerIdForJobName —
|
|
29
29
|
// a QN like "publicstatus:job:uptime-probe" must not leave ":" in the id.
|
|
30
30
|
export function bootJobIdForJobName(jobName) {
|
|
31
31
|
return `boot-${jobName.replace(/[.:]/g, "-")}`;
|
|
@@ -97,8 +97,7 @@ export async function removeOrphanJobSchedulers(queue, expectedSchedulerIds, lan
|
|
|
97
97
|
// duplicates — safe only as long as the child job hash outlives the
|
|
98
98
|
// wrapper's retry window, enforced at construction against
|
|
99
99
|
// COMPLETED_JOB_RETENTION_AGE_SEC (see the invariant check in
|
|
100
|
-
// createJobRunner).
|
|
101
|
-
// (fw#1603/#1604) — the wrapper id already contains ":", so strip
|
|
100
|
+
// createJobRunner). The wrapper id already contains ":", so strip
|
|
102
101
|
// separators from both halves before joining instead of interpolating raw.
|
|
103
102
|
export function perTenantChildJobId(wrapperJobId, tenantId) {
|
|
104
103
|
const sanitize = (value) => value.replace(/[.:]/g, "-");
|
|
@@ -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
|
+
// `activeOnly` + `self` serve the defer check, which runs inside the job being
|
|
17
|
+
// counted: it must not see waiting jobs or itself, and only counts jobs that
|
|
18
|
+
// became active before it. Without that order, workers that fetch a burst of
|
|
19
|
+
// jobs together all see each other as active and all defer, again and again.
|
|
20
|
+
export async function isOverPerTenantLimit(scope, jobName, tenantId, max, countFilter = {}) {
|
|
21
|
+
const { queues } = scope;
|
|
22
|
+
const results = await Promise.all([
|
|
23
|
+
queues.api.getActive(),
|
|
24
|
+
queues.worker.getActive(),
|
|
25
|
+
...(countFilter.activeOnly ? [] : [queues.api.getWaiting(), queues.worker.getWaiting()]),
|
|
26
|
+
]);
|
|
27
|
+
let count = 0;
|
|
28
|
+
for (const list of results) {
|
|
29
|
+
for (const j of list) {
|
|
30
|
+
if (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";
|