@cosmicdrift/kumiko-framework 0.13.0 → 0.15.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/package.json +7 -7
- package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
- package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
- package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
- package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
- package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
- package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
- package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
- package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
- package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/csrf-middleware.test.ts +1 -1
- package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
- package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
- package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
- package/src/api/__tests__/readiness.test.ts +1 -1
- package/src/api/__tests__/request-id-middleware.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +12 -12
- package/src/api/__tests__/sse-route.test.ts +1 -1
- package/src/api/readiness.ts +2 -2
- package/src/auth/__tests__/roles.test.ts +2 -2
- package/src/bun-db/__tests__/PATTERN.md +73 -0
- package/src/bun-db/__tests__/_helpers.ts +103 -0
- package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
- package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
- package/src/bun-db/__tests__/bun-test-db.ts +19 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
- package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
- package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
- package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
- package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
- package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
- package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
- package/src/bun-db/connection.ts +84 -0
- package/src/bun-db/index.ts +31 -0
- package/src/bun-db/query.ts +845 -0
- package/src/compliance/__tests__/duration-spec.test.ts +1 -1
- package/src/compliance/__tests__/profiles.test.ts +1 -1
- package/src/compliance/__tests__/sub-processors.test.ts +1 -1
- package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
- package/src/db/__tests__/big-int-field.test.ts +15 -14
- package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
- package/src/db/__tests__/connection-options.test.ts +1 -1
- package/src/db/__tests__/dialect-instant.test.ts +1 -1
- package/src/db/__tests__/encryption.test.ts +1 -1
- package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
- package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
- package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
- package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
- package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
- package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
- package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
- package/src/db/__tests__/sql-inventory.test.ts +56 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
- package/src/db/__tests__/table-builder-required.test.ts +20 -22
- package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
- package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
- package/src/db/api.ts +46 -0
- package/src/db/apply-entity-event.ts +45 -36
- package/src/db/assert-exists-in.ts +5 -16
- package/src/db/bun-provider.ts +37 -0
- package/src/db/config-seed.ts +4 -4
- package/src/db/connection.ts +14 -57
- package/src/db/cursor.ts +5 -56
- package/src/db/dialect.ts +472 -99
- package/src/db/eagerload.ts +5 -12
- package/src/db/entity-table-meta.ts +390 -0
- package/src/db/event-store-executor.ts +158 -100
- package/src/db/index.ts +33 -5
- package/src/db/migrate-generator.ts +350 -0
- package/src/db/migrate-runner.ts +206 -0
- package/src/db/postgres-provider.ts +25 -0
- package/src/db/queries/entity-read.ts +15 -0
- package/src/db/queries/es-ops.ts +17 -0
- package/src/db/queries/event-consumer.ts +170 -0
- package/src/db/queries/event-store-admin.ts +127 -0
- package/src/db/queries/event-store.ts +155 -0
- package/src/db/queries/projection-rebuild.ts +59 -0
- package/src/db/queries/raw-sql.ts +15 -0
- package/src/db/queries/schema-drift.ts +35 -0
- package/src/db/queries/seed-context.ts +58 -0
- package/src/db/queries/table-ops.ts +11 -0
- package/src/db/queries/test-stack.ts +56 -0
- package/src/db/query-api.ts +22 -0
- package/src/db/query.ts +30 -0
- package/src/db/reference-data.ts +19 -22
- package/src/db/render-ddl.ts +57 -0
- package/src/db/row-helpers.ts +3 -52
- package/src/db/schema-inspection.ts +17 -4
- package/src/db/sql-inventory.ts +208 -0
- package/src/db/table-builder.ts +48 -40
- package/src/db/tenant-db.ts +105 -326
- package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
- package/src/engine/__tests__/boot-validator.test.ts +4 -3
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-target.test.ts +1 -1
- package/src/engine/__tests__/claim-keys.test.ts +1 -1
- package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
- package/src/engine/__tests__/config-helpers.test.ts +1 -1
- package/src/engine/__tests__/effective-features.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +3 -3
- package/src/engine/__tests__/extends-registrar.test.ts +4 -4
- package/src/engine/__tests__/factories-long-text.test.ts +1 -1
- package/src/engine/__tests__/factories-time.test.ts +1 -1
- package/src/engine/__tests__/field-predicates.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/identifiers.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/ownership.test.ts +10 -11
- package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
- package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
- package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-helpers.test.ts +25 -17
- package/src/engine/__tests__/projection.test.ts +4 -4
- package/src/engine/__tests__/qualified-name.test.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +9 -8
- package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
- package/src/engine/__tests__/run-in.test.ts +1 -1
- package/src/engine/__tests__/schema-builder.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
- package/src/engine/__tests__/state-machine.test.ts +1 -1
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
- package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
- package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
- package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
- package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
- package/src/engine/__tests__/steps-read.test.ts +34 -40
- package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
- package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
- package/src/engine/__tests__/steps-workflow.test.ts +7 -7
- package/src/engine/__tests__/system-user.test.ts +1 -1
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
- package/src/engine/__tests__/validation-hooks.test.ts +1 -1
- package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
- package/src/engine/boot-validator/entity-handler.ts +3 -3
- package/src/engine/boot-validator/ownership.ts +1 -1
- package/src/engine/define-feature.ts +1 -2
- package/src/engine/entity-handlers.ts +5 -5
- package/src/engine/factories.ts +1 -1
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
- package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
- package/src/engine/ownership.ts +113 -41
- package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
- package/src/engine/projection-helpers.ts +2 -11
- package/src/engine/registry.ts +2 -2
- package/src/engine/steps/read-find-many.ts +13 -13
- package/src/engine/steps/read-find-one.ts +7 -9
- package/src/engine/steps/unsafe-projection-delete.ts +4 -5
- package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
- package/src/engine/types/feature.ts +7 -2
- package/src/engine/types/fields.ts +4 -5
- package/src/engine/types/step.ts +10 -10
- package/src/engine/validate-projection-allowlist.ts +23 -3
- package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
- package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/errors/__tests__/write-failures.test.ts +1 -1
- package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
- package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
- package/src/es-ops/__tests__/runner.test.ts +29 -19
- package/src/es-ops/context.ts +9 -43
- package/src/es-ops/operations-schema.ts +2 -2
- package/src/es-ops/runner.ts +12 -26
- package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
- package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
- package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
- package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
- package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
- package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
- package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
- package/src/event-store/admin-api.ts +55 -83
- package/src/event-store/archive.ts +15 -39
- package/src/event-store/event-store.ts +92 -86
- package/src/event-store/events-schema.ts +2 -1
- package/src/event-store/index.ts +1 -0
- package/src/event-store/snapshot.ts +26 -24
- package/src/event-store/upcaster-dead-letter.ts +19 -18
- package/src/files/__tests__/content-disposition.test.ts +1 -1
- package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
- package/src/files/__tests__/file-handle.test.ts +1 -1
- package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
- package/src/files/__tests__/read-stream.test.ts +1 -1
- package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
- package/src/files/__tests__/write-stream.test.ts +1 -1
- package/src/files/__tests__/zip-stream.test.ts +1 -1
- package/src/files/file-ref-table.ts +2 -2
- package/src/files/file-routes.ts +7 -9
- package/src/files/storage-tracking.ts +9 -17
- package/src/i18n/__tests__/i18n.test.ts +1 -1
- package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
- package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
- package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
- package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
- package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
- package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
- package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
- package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
- package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
- package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
- package/src/migrations/projection-detection.ts +12 -1
- package/src/migrations/schema-drift.ts +7 -23
- package/src/observability/__tests__/console-provider.test.ts +1 -1
- package/src/observability/__tests__/metric-validator.test.ts +1 -1
- package/src/observability/__tests__/noop-provider.test.ts +1 -1
- package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
- package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-meter.test.ts +1 -1
- package/src/observability/__tests__/recording-tracer.test.ts +1 -1
- package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
- package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
- package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
- package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
- package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
- package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
- package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
- package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
- package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
- package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
- package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
- package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
- package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
- package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
- package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
- package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
- package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
- package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
- package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
- package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
- package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
- package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
- package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
- package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
- package/src/pipeline/cascade-handler.ts +13 -21
- package/src/pipeline/dispatcher.ts +43 -48
- package/src/pipeline/event-consumer-state.ts +11 -2
- package/src/pipeline/event-dispatcher.ts +86 -146
- package/src/pipeline/event-retention.ts +14 -24
- package/src/pipeline/msp-rebuild.ts +54 -78
- package/src/pipeline/projection-rebuild.ts +65 -67
- package/src/pipeline/projection-state.ts +2 -2
- package/src/random/__tests__/generate.test.ts +13 -13
- package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
- package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
- package/src/redis/__tests__/redis-options.test.ts +1 -1
- package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
- package/src/search/__tests__/search-adapter.test.ts +1 -1
- package/src/secrets/__tests__/dek-cache.test.ts +1 -3
- package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +1 -1
- package/src/secrets/__tests__/rotation.test.ts +1 -1
- package/src/stack/db.ts +25 -48
- package/src/stack/push-entity-projection-tables.ts +2 -4
- package/src/stack/table-helpers.ts +98 -61
- package/src/stack/test-stack.ts +8 -7
- package/src/testing/__tests__/db-cleanup.test.ts +40 -0
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
- package/src/testing/db-cleanup.ts +44 -0
- package/src/testing/expect-error.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/multipart-helper.ts +94 -0
- package/src/testing/shared-entities.ts +5 -5
- package/src/time/__tests__/polyfill.test.ts +1 -1
- package/src/time/__tests__/tz-context.test.ts +1 -1
- package/src/utils/__tests__/assert.test.ts +1 -1
- package/src/utils/__tests__/env-parse.test.ts +1 -1
- package/CHANGELOG.md +0 -472
- package/src/db/__tests__/cursor.test.ts +0 -41
- package/src/db/__tests__/db-helpers.test.ts +0 -369
- package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
- package/src/db/__tests__/row-helpers.test.ts +0 -59
- package/src/engine/steps/_drizzle-boundary.ts +0 -19
- package/src/files/__tests__/file-field-column.integration.ts +0 -103
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { and, asc, eq, gt, sql } from "drizzle-orm";
|
|
2
1
|
import { requestContext } from "../api/request-context";
|
|
3
2
|
import type { DbConnection, DbTx, PgClient } from "../db/connection";
|
|
3
|
+
import {
|
|
4
|
+
advanceConsumerPastEventReturning,
|
|
5
|
+
insertConsumerIfAbsent,
|
|
6
|
+
markConsumerProcessing,
|
|
7
|
+
selectConsumerForUpdateSkipLocked,
|
|
8
|
+
updateConsumerDeliveryOutcome,
|
|
9
|
+
updateConsumerStatusReturning,
|
|
10
|
+
} from "../db/queries/event-consumer";
|
|
11
|
+
import { selectEventsHeadId, selectNextEventIdAfter } from "../db/queries/event-store";
|
|
12
|
+
import { coerceRow, extractTableInfo, selectMany } from "../db/query";
|
|
4
13
|
import type { AppContext } from "../engine/types";
|
|
5
14
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers";
|
|
6
15
|
import {
|
|
@@ -154,7 +163,30 @@ const DEFAULT_MAX_ATTEMPTS = 10;
|
|
|
154
163
|
// dispatcher's main pass logic stays under ~50 LOC. Every helper takes an
|
|
155
164
|
// explicit `tx` — none of them use the outer dispatcher's closure state.
|
|
156
165
|
|
|
157
|
-
type
|
|
166
|
+
type ConsumerStateRowShape = {
|
|
167
|
+
readonly name: string;
|
|
168
|
+
readonly instanceId: string;
|
|
169
|
+
readonly lastProcessedEventId: bigint;
|
|
170
|
+
readonly status: string;
|
|
171
|
+
readonly attempts: number;
|
|
172
|
+
readonly lastError: string | null;
|
|
173
|
+
readonly updatedAt: Temporal.Instant;
|
|
174
|
+
};
|
|
175
|
+
type ConsumerStateRow = ConsumerStateRowShape;
|
|
176
|
+
|
|
177
|
+
type StoredEventRow = {
|
|
178
|
+
readonly id: bigint;
|
|
179
|
+
readonly aggregateId: string;
|
|
180
|
+
readonly aggregateType: string;
|
|
181
|
+
readonly tenantId: string;
|
|
182
|
+
readonly version: number;
|
|
183
|
+
readonly type: string;
|
|
184
|
+
readonly eventVersion: number;
|
|
185
|
+
readonly payload: Record<string, unknown>;
|
|
186
|
+
readonly metadata: import("../event-store/event-store").EventMetadata;
|
|
187
|
+
readonly createdAt: Temporal.Instant;
|
|
188
|
+
readonly createdBy: string;
|
|
189
|
+
};
|
|
158
190
|
|
|
159
191
|
type AcquireOutcome =
|
|
160
192
|
| { readonly state: ConsumerStateRow; readonly skip: null }
|
|
@@ -180,16 +212,13 @@ async function acquireConsumerState(
|
|
|
180
212
|
name: string,
|
|
181
213
|
instanceId: string,
|
|
182
214
|
): Promise<AcquireOutcome> {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
),
|
|
191
|
-
)
|
|
192
|
-
.for("update", { skipLocked: true })) as [ConsumerStateRow | undefined]; // @cast-boundary db-row
|
|
215
|
+
const rawState = await selectConsumerForUpdateSkipLocked(tx, name, instanceId);
|
|
216
|
+
|
|
217
|
+
if (!rawState) {
|
|
218
|
+
return { state: null, skip: "not_registered" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow;
|
|
193
222
|
|
|
194
223
|
if (!state) {
|
|
195
224
|
// Either the row never existed (no pre-reg, no ensureRegistered) or
|
|
@@ -218,12 +247,7 @@ async function preRegisterConsumers(
|
|
|
218
247
|
): Promise<void> {
|
|
219
248
|
for (const consumer of consumers) {
|
|
220
249
|
const instanceId = consumerInstanceId(consumer, dispatcherInstanceId);
|
|
221
|
-
await db
|
|
222
|
-
.insert(eventConsumerStateTable)
|
|
223
|
-
.values({ name: consumer.name, instanceId, status: "idle" })
|
|
224
|
-
.onConflictDoNothing({
|
|
225
|
-
target: [eventConsumerStateTable.name, eventConsumerStateTable.instanceId],
|
|
226
|
-
});
|
|
250
|
+
await insertConsumerIfAbsent(db, consumer.name, instanceId);
|
|
227
251
|
}
|
|
228
252
|
}
|
|
229
253
|
|
|
@@ -249,28 +273,20 @@ function consumerInstanceId(
|
|
|
249
273
|
// lock already guarantees single-writer semantics; this is purely
|
|
250
274
|
// informational (and resets on commit to idle/dead via persistConsumerOutcome).
|
|
251
275
|
async function markProcessing(tx: DbTx, name: string, instanceId: string): Promise<void> {
|
|
252
|
-
await tx
|
|
253
|
-
.update(eventConsumerStateTable)
|
|
254
|
-
.set({ status: "processing", updatedAt: sql`now()` })
|
|
255
|
-
.where(
|
|
256
|
-
and(
|
|
257
|
-
eq(eventConsumerStateTable.name, name),
|
|
258
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
259
|
-
),
|
|
260
|
-
);
|
|
276
|
+
await markConsumerProcessing(tx, name, instanceId);
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
async function fetchPendingEvents(
|
|
264
280
|
tx: DbTx,
|
|
265
281
|
cursor: bigint,
|
|
266
282
|
batchSize: number,
|
|
267
|
-
): Promise<ReadonlyArray<
|
|
268
|
-
return (await
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
): Promise<ReadonlyArray<StoredEventRow>> {
|
|
284
|
+
return (await selectMany(
|
|
285
|
+
tx,
|
|
286
|
+
eventsTable,
|
|
287
|
+
{ id: { gt: cursor } },
|
|
288
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: batchSize },
|
|
289
|
+
)) as ReadonlyArray<StoredEventRow>; // @cast-boundary db-row
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
type DeliveryOutcome = {
|
|
@@ -282,7 +298,7 @@ type DeliveryOutcome = {
|
|
|
282
298
|
readonly failed: number;
|
|
283
299
|
};
|
|
284
300
|
|
|
285
|
-
function rowToStoredEvent(row:
|
|
301
|
+
function rowToStoredEvent(row: StoredEventRow): StoredEvent {
|
|
286
302
|
return {
|
|
287
303
|
id: String(row.id),
|
|
288
304
|
aggregateId: row.aggregateId,
|
|
@@ -305,7 +321,7 @@ function rowToStoredEvent(row: typeof eventsTable.$inferSelect): StoredEvent {
|
|
|
305
321
|
// restartConsumer / skipPoisonEvent).
|
|
306
322
|
async function deliverEvents(
|
|
307
323
|
consumer: EventConsumer,
|
|
308
|
-
events: ReadonlyArray<
|
|
324
|
+
events: ReadonlyArray<StoredEventRow>,
|
|
309
325
|
context: AppContext,
|
|
310
326
|
maxAttempts: number,
|
|
311
327
|
state: ConsumerStateRow,
|
|
@@ -376,21 +392,7 @@ async function persistConsumerOutcome(
|
|
|
376
392
|
instanceId: string,
|
|
377
393
|
outcome: DeliveryOutcome,
|
|
378
394
|
): Promise<void> {
|
|
379
|
-
await tx
|
|
380
|
-
.update(eventConsumerStateTable)
|
|
381
|
-
.set({
|
|
382
|
-
lastProcessedEventId: outcome.cursor,
|
|
383
|
-
attempts: outcome.attempts,
|
|
384
|
-
status: outcome.deadLettered ? "dead" : "idle",
|
|
385
|
-
lastError: outcome.lastError,
|
|
386
|
-
updatedAt: sql`now()`,
|
|
387
|
-
})
|
|
388
|
-
.where(
|
|
389
|
-
and(
|
|
390
|
-
eq(eventConsumerStateTable.name, name),
|
|
391
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
392
|
-
),
|
|
393
|
-
);
|
|
395
|
+
await updateConsumerDeliveryOutcome(tx, name, instanceId, outcome);
|
|
394
396
|
}
|
|
395
397
|
|
|
396
398
|
// Emit the lag gauge inside the consumer pass's tx so ops sees a snapshot
|
|
@@ -403,13 +405,7 @@ async function emitLagFromTx(
|
|
|
403
405
|
cursor: bigint,
|
|
404
406
|
meter: Meter,
|
|
405
407
|
): Promise<void> {
|
|
406
|
-
const
|
|
407
|
-
sql`SELECT COALESCE(MAX(id), 0)::bigint AS head FROM kumiko_events`,
|
|
408
|
-
);
|
|
409
|
-
// @cast-boundary db-row — raw drizzle.execute() COALESCE-aggregate row
|
|
410
|
-
const rows = Array.isArray(result) ? (result as Array<{ head?: bigint | string | null }>) : []; // @cast-boundary db-row
|
|
411
|
-
const raw = rows[0]?.head;
|
|
412
|
-
const head = typeof raw === "bigint" ? raw : BigInt(raw ?? 0);
|
|
408
|
+
const head = await selectEventsHeadId(tx);
|
|
413
409
|
const lag = head > cursor ? Number(head - cursor) : 0;
|
|
414
410
|
emitEventConsumerLag(meter, { consumer: consumerName, instanceId }, lag);
|
|
415
411
|
}
|
|
@@ -535,7 +531,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
|
|
|
535
531
|
});
|
|
536
532
|
|
|
537
533
|
try {
|
|
538
|
-
await db.
|
|
534
|
+
await db.begin(async (tx: DbTx) => {
|
|
539
535
|
const acquired = await acquireConsumerState(tx, consumer.name, instanceId);
|
|
540
536
|
// skip: another instance holds the lock, or the consumer is
|
|
541
537
|
// disabled/dead. Nothing to deliver this pass.
|
|
@@ -701,9 +697,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
|
|
|
701
697
|
// lastError=null, status="idle". For events that will
|
|
702
698
|
// never succeed (broken payload, removed feature code).
|
|
703
699
|
|
|
704
|
-
function normalizeConsumerState(
|
|
705
|
-
row: typeof eventConsumerStateTable.$inferSelect,
|
|
706
|
-
): ConsumerRecoveryState {
|
|
700
|
+
function normalizeConsumerState(row: ConsumerStateRowShape): ConsumerRecoveryState {
|
|
707
701
|
return {
|
|
708
702
|
name: row.name,
|
|
709
703
|
instanceId: row.instanceId,
|
|
@@ -735,16 +729,11 @@ async function requireConsumerRow(
|
|
|
735
729
|
db: DbConnection,
|
|
736
730
|
name: string,
|
|
737
731
|
instanceId: string,
|
|
738
|
-
): Promise<
|
|
739
|
-
const [row] = await db
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
and(
|
|
744
|
-
eq(eventConsumerStateTable.name, name),
|
|
745
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
746
|
-
),
|
|
747
|
-
);
|
|
732
|
+
): Promise<ConsumerStateRowShape> {
|
|
733
|
+
const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
|
|
734
|
+
name,
|
|
735
|
+
instanceId,
|
|
736
|
+
});
|
|
748
737
|
if (!row) {
|
|
749
738
|
throw new Error(
|
|
750
739
|
`Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
|
|
@@ -765,16 +754,9 @@ export async function restartConsumer(
|
|
|
765
754
|
`Consumer "${name}" (instance_id="${instanceId}") is not dead (status="${before.status}"). Restart only applies to dead consumers; use "enable" for a disabled one.`,
|
|
766
755
|
);
|
|
767
756
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
.where(
|
|
772
|
-
and(
|
|
773
|
-
eq(eventConsumerStateTable.name, name),
|
|
774
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
775
|
-
),
|
|
776
|
-
)
|
|
777
|
-
.returning();
|
|
757
|
+
const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
|
|
758
|
+
const updated =
|
|
759
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
778
760
|
if (!updated) {
|
|
779
761
|
throw new Error(
|
|
780
762
|
`Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
|
|
@@ -789,16 +771,9 @@ export async function disableConsumer(
|
|
|
789
771
|
instanceId: string = SHARED_INSTANCE_SENTINEL,
|
|
790
772
|
): Promise<ConsumerRecoveryState> {
|
|
791
773
|
await requireConsumerRow(db, name, instanceId);
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
.where(
|
|
796
|
-
and(
|
|
797
|
-
eq(eventConsumerStateTable.name, name),
|
|
798
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
799
|
-
),
|
|
800
|
-
)
|
|
801
|
-
.returning();
|
|
774
|
+
const raw = await updateConsumerStatusReturning(db, name, instanceId, "disabled");
|
|
775
|
+
const updated =
|
|
776
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
802
777
|
if (!updated) {
|
|
803
778
|
throw new Error(
|
|
804
779
|
`Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
|
|
@@ -818,16 +793,9 @@ export async function enableConsumer(
|
|
|
818
793
|
`Consumer "${name}" (instance_id="${instanceId}") is not disabled (status="${before.status}"). Enable only flips disabled → idle; use "restart" for a dead consumer.`,
|
|
819
794
|
);
|
|
820
795
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
.where(
|
|
825
|
-
and(
|
|
826
|
-
eq(eventConsumerStateTable.name, name),
|
|
827
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
828
|
-
),
|
|
829
|
-
)
|
|
830
|
-
.returning();
|
|
796
|
+
const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
|
|
797
|
+
const updated =
|
|
798
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
831
799
|
if (!updated) {
|
|
832
800
|
throw new Error(
|
|
833
801
|
`Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
|
|
@@ -846,48 +814,25 @@ export async function skipPoisonEvent(
|
|
|
846
814
|
instanceId: string = SHARED_INSTANCE_SENTINEL,
|
|
847
815
|
): Promise<ConsumerRecoveryState & { readonly skippedEventId: bigint | null }> {
|
|
848
816
|
const before = await requireConsumerRow(db, name, instanceId);
|
|
849
|
-
return db.
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (!poison) {
|
|
857
|
-
const [unchanged] = await tx
|
|
858
|
-
.select()
|
|
859
|
-
.from(eventConsumerStateTable)
|
|
860
|
-
.where(
|
|
861
|
-
and(
|
|
862
|
-
eq(eventConsumerStateTable.name, name),
|
|
863
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
864
|
-
),
|
|
865
|
-
);
|
|
817
|
+
return db.begin(async (tx: DbTx) => {
|
|
818
|
+
const poisonId = await selectNextEventIdAfter(tx, before.lastProcessedEventId);
|
|
819
|
+
if (poisonId === null) {
|
|
820
|
+
const [unchanged] = await selectMany<ConsumerStateRow>(tx, eventConsumerStateTable, {
|
|
821
|
+
name,
|
|
822
|
+
instanceId,
|
|
823
|
+
});
|
|
866
824
|
if (!unchanged)
|
|
867
825
|
throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished — retry.`);
|
|
868
826
|
return { ...normalizeConsumerState(unchanged), skippedEventId: null };
|
|
869
827
|
}
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
lastProcessedEventId: poison.id,
|
|
874
|
-
status: "idle",
|
|
875
|
-
attempts: 0,
|
|
876
|
-
lastError: null,
|
|
877
|
-
updatedAt: sql`now()`,
|
|
878
|
-
})
|
|
879
|
-
.where(
|
|
880
|
-
and(
|
|
881
|
-
eq(eventConsumerStateTable.name, name),
|
|
882
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
883
|
-
),
|
|
884
|
-
)
|
|
885
|
-
.returning();
|
|
828
|
+
const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId);
|
|
829
|
+
const updated =
|
|
830
|
+
raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
|
|
886
831
|
if (!updated)
|
|
887
832
|
throw new Error(
|
|
888
833
|
`Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`,
|
|
889
834
|
);
|
|
890
|
-
return { ...normalizeConsumerState(updated), skippedEventId:
|
|
835
|
+
return { ...normalizeConsumerState(updated), skippedEventId: poisonId };
|
|
891
836
|
});
|
|
892
837
|
}
|
|
893
838
|
|
|
@@ -905,15 +850,10 @@ export async function getConsumerState(
|
|
|
905
850
|
readonly lastError: string | null;
|
|
906
851
|
readonly updatedAt: Temporal.Instant;
|
|
907
852
|
} | null> {
|
|
908
|
-
const [row] = await db
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
and(
|
|
913
|
-
eq(eventConsumerStateTable.name, name),
|
|
914
|
-
eq(eventConsumerStateTable.instanceId, instanceId),
|
|
915
|
-
),
|
|
916
|
-
);
|
|
853
|
+
const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
|
|
854
|
+
name,
|
|
855
|
+
instanceId,
|
|
856
|
+
});
|
|
917
857
|
if (!row) return null;
|
|
918
858
|
return {
|
|
919
859
|
name: row.name,
|
|
@@ -947,7 +887,7 @@ export async function listConsumersWithState(
|
|
|
947
887
|
readonly lastError: string | null;
|
|
948
888
|
}>
|
|
949
889
|
> {
|
|
950
|
-
const stateRows = await db
|
|
890
|
+
const stateRows = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable);
|
|
951
891
|
const registered = new Set(registeredNames);
|
|
952
892
|
|
|
953
893
|
// Materialize one output row per (name, instance_id). Registered names
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { and, getTableName, inArray, lt, sql } from "drizzle-orm";
|
|
2
1
|
import type { DbConnection } from "../db/connection";
|
|
2
|
+
import { lockEventConsumersShareMode } from "../db/queries/event-consumer";
|
|
3
|
+
import { deleteMany, selectMany, transaction } from "../db/query";
|
|
3
4
|
import { eventsTable } from "../event-store";
|
|
4
5
|
import { eventConsumerStateTable } from "./event-consumer-state";
|
|
5
6
|
|
|
@@ -83,7 +84,7 @@ export async function pruneEvents(
|
|
|
83
84
|
const aggregateTypes = options.aggregateTypes;
|
|
84
85
|
const dryRun = options.dryRun === true;
|
|
85
86
|
|
|
86
|
-
return
|
|
87
|
+
return transaction(db, async (tx) => {
|
|
87
88
|
// Serialise against consumer-bootstrap INSERTs. Without this, the race
|
|
88
89
|
// is: prune reads consumers (snapshot misses a consumer bootstrapping
|
|
89
90
|
// in a parallel tx) → consumer commits its row with
|
|
@@ -96,21 +97,13 @@ export async function pruneEvents(
|
|
|
96
97
|
// (cursor advances) do too, but prune is measured in milliseconds and
|
|
97
98
|
// pausing cursor advances for that window is cheap insurance against
|
|
98
99
|
// a silent data-loss bug.
|
|
99
|
-
|
|
100
|
-
// Drizzle can't express LOCK TABLE — drop to raw SQL with the table
|
|
101
|
-
// name identifier so a future table-rename is caught at compile time.
|
|
102
|
-
await tx.execute(sql.raw(`LOCK TABLE ${getTableName(eventConsumerStateTable)} IN SHARE MODE`));
|
|
100
|
+
await lockEventConsumersShareMode(tx);
|
|
103
101
|
|
|
104
102
|
// Step 1 — collect candidate event ids.
|
|
105
|
-
const candidates = await tx
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
and(
|
|
110
|
-
inArray(eventsTable.aggregateType, [...aggregateTypes]),
|
|
111
|
-
lt(eventsTable.createdAt, cutoff),
|
|
112
|
-
),
|
|
113
|
-
);
|
|
103
|
+
const candidates = await selectMany<{ id: bigint }>(tx, eventsTable, {
|
|
104
|
+
aggregateType: [...aggregateTypes],
|
|
105
|
+
createdAt: { lt: cutoff },
|
|
106
|
+
});
|
|
114
107
|
|
|
115
108
|
if (candidates.length === 0) {
|
|
116
109
|
return { deletedCount: 0, cutoff, aggregateTypes, dryRun };
|
|
@@ -127,10 +120,10 @@ export async function pruneEvents(
|
|
|
127
120
|
// The SHARE lock above guarantees this SELECT sees a complete view:
|
|
128
121
|
// no new consumer can INSERT a fresh-cursor row between here and the
|
|
129
122
|
// DELETE below.
|
|
130
|
-
const activeConsumers = await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
const activeConsumers = await selectMany<{
|
|
124
|
+
name: string;
|
|
125
|
+
lastProcessedEventId: bigint;
|
|
126
|
+
}>(tx, eventConsumerStateTable, { status: { ne: "disabled" } });
|
|
134
127
|
|
|
135
128
|
for (const consumer of activeConsumers) {
|
|
136
129
|
if (consumer.lastProcessedEventId < maxCandidateId) {
|
|
@@ -144,11 +137,8 @@ export async function pruneEvents(
|
|
|
144
137
|
|
|
145
138
|
// Step 3 — actual delete, bounded to the candidate set.
|
|
146
139
|
const candidateIds = candidates.map((c) => c.id);
|
|
147
|
-
|
|
148
|
-
.delete(eventsTable)
|
|
149
|
-
.where(inArray(eventsTable.id, candidateIds))
|
|
150
|
-
.returning({ id: eventsTable.id });
|
|
140
|
+
await deleteMany(tx, eventsTable, { id: candidateIds });
|
|
151
141
|
|
|
152
|
-
return { deletedCount:
|
|
142
|
+
return { deletedCount: candidateIds.length, cutoff, aggregateTypes, dryRun: false };
|
|
153
143
|
});
|
|
154
144
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { DbConnection, DbRunner, DbTx } from "../db/connection";
|
|
2
|
+
import {
|
|
3
|
+
markConsumerRebuildFailed,
|
|
4
|
+
resetConsumerForMspRebuild,
|
|
5
|
+
selectConsumerForUpdate,
|
|
6
|
+
updateConsumerRebuildCursor,
|
|
7
|
+
} from "../db/queries/event-consumer";
|
|
8
|
+
import { truncateTable } from "../db/queries/table-ops";
|
|
9
|
+
import { selectMany } from "../db/query";
|
|
3
10
|
import type { Registry, TenantId } from "../engine/types";
|
|
4
11
|
import { InternalError } from "../errors";
|
|
5
12
|
import { eventsTable, type StoredEvent, upcastStoredEvent } from "../event-store";
|
|
@@ -7,7 +14,7 @@ import { loadAggregate, loadAggregateAsOf } from "../event-store/event-store";
|
|
|
7
14
|
import { upcastStoredEvents } from "../event-store/upcaster";
|
|
8
15
|
import { emitProjectionRebuild } from "../observability/standard-metrics";
|
|
9
16
|
import type { Meter } from "../observability/types/metric";
|
|
10
|
-
import {
|
|
17
|
+
import { SHARED_INSTANCE_SENTINEL } from "./event-consumer-state";
|
|
11
18
|
import type { MultiStreamApplyContext } from "./multi-stream-apply-context";
|
|
12
19
|
import type { RebuildResult } from "./projection-rebuild";
|
|
13
20
|
|
|
@@ -101,57 +108,35 @@ export async function rebuildMultiStreamProjection(
|
|
|
101
108
|
let lastProcessedEventId = 0n;
|
|
102
109
|
|
|
103
110
|
try {
|
|
104
|
-
await db.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// table, so the guard above refuses them anyway), and rebuild's
|
|
108
|
-
// purpose is to rematerialize one persistent read-model, not fan
|
|
109
|
-
// out a local cache reset across instances. The FOR UPDATE on the
|
|
110
|
-
// next SELECT is what blocks concurrent rebuilds of the same MSP;
|
|
111
|
-
// live dispatcher passes use SKIP LOCKED on this row and will bail
|
|
112
|
-
// silently while we hold it.
|
|
113
|
-
await tx
|
|
114
|
-
.insert(eventConsumerStateTable)
|
|
115
|
-
.values({
|
|
116
|
-
name: mspName,
|
|
117
|
-
instanceId: SHARED_INSTANCE_SENTINEL,
|
|
118
|
-
lastProcessedEventId: 0n,
|
|
119
|
-
status: "idle",
|
|
120
|
-
})
|
|
121
|
-
.onConflictDoUpdate({
|
|
122
|
-
target: [eventConsumerStateTable.name, eventConsumerStateTable.instanceId],
|
|
123
|
-
set: {
|
|
124
|
-
lastProcessedEventId: 0n,
|
|
125
|
-
status: "idle",
|
|
126
|
-
attempts: 0,
|
|
127
|
-
lastError: null,
|
|
128
|
-
updatedAt: sql`now()`,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
await tx
|
|
132
|
-
.select()
|
|
133
|
-
.from(eventConsumerStateTable)
|
|
134
|
-
.where(
|
|
135
|
-
and(
|
|
136
|
-
eq(eventConsumerStateTable.name, mspName),
|
|
137
|
-
eq(eventConsumerStateTable.instanceId, SHARED_INSTANCE_SENTINEL),
|
|
138
|
-
),
|
|
139
|
-
)
|
|
140
|
-
.for("update");
|
|
111
|
+
await db.begin(async (tx: DbTx) => {
|
|
112
|
+
await resetConsumerForMspRebuild(tx, mspName, SHARED_INSTANCE_SENTINEL);
|
|
113
|
+
await selectConsumerForUpdate(tx, mspName, SHARED_INSTANCE_SENTINEL);
|
|
141
114
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const tableName = getTableName(msp.table as NonNullable<typeof msp.table>); // @cast-boundary db-operator
|
|
146
|
-
await tx.execute(sql.raw(`TRUNCATE TABLE ${quoteIdent(tableName)}`));
|
|
115
|
+
const mspTable = msp.table as NonNullable<typeof msp.table>;
|
|
116
|
+
const tableName = getTableName(mspTable);
|
|
117
|
+
await truncateTable(tx, tableName);
|
|
147
118
|
|
|
148
119
|
const subscribedTypes = Object.keys(msp.apply);
|
|
149
120
|
if (subscribedTypes.length > 0) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
121
|
+
type EventRow = {
|
|
122
|
+
id: bigint;
|
|
123
|
+
aggregateId: string;
|
|
124
|
+
aggregateType: string;
|
|
125
|
+
tenantId: TenantId;
|
|
126
|
+
version: number;
|
|
127
|
+
type: string;
|
|
128
|
+
eventVersion: number;
|
|
129
|
+
payload: Record<string, unknown>;
|
|
130
|
+
metadata: import("../event-store/event-store").EventMetadata;
|
|
131
|
+
createdAt: Temporal.Instant;
|
|
132
|
+
createdBy: string;
|
|
133
|
+
};
|
|
134
|
+
const events = await selectMany<EventRow>(
|
|
135
|
+
tx,
|
|
136
|
+
eventsTable,
|
|
137
|
+
{ type: [...subscribedTypes] },
|
|
138
|
+
{ orderBy: { col: "id", direction: "asc" } },
|
|
139
|
+
);
|
|
155
140
|
|
|
156
141
|
const upcasters = registry.getEventUpcasters();
|
|
157
142
|
for (const row of events) {
|
|
@@ -170,44 +155,27 @@ export async function rebuildMultiStreamProjection(
|
|
|
170
155
|
};
|
|
171
156
|
const storedEvent = await upcastStoredEvent(raw, upcasters, {
|
|
172
157
|
db: tx,
|
|
173
|
-
tenantId: row.tenantId
|
|
158
|
+
tenantId: row.tenantId,
|
|
174
159
|
});
|
|
175
160
|
const applyFn = msp.apply[row.type];
|
|
176
161
|
if (!applyFn) continue;
|
|
177
|
-
const rebuildCtx = createRebuildCtx(registry, tx, row.tenantId
|
|
162
|
+
const rebuildCtx = createRebuildCtx(registry, tx, row.tenantId);
|
|
178
163
|
await applyFn(storedEvent, tx, rebuildCtx);
|
|
179
164
|
eventsProcessed++;
|
|
180
165
|
lastProcessedEventId = row.id;
|
|
181
166
|
}
|
|
182
167
|
}
|
|
183
168
|
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
lastError: null,
|
|
191
|
-
updatedAt: sql`now()`,
|
|
192
|
-
})
|
|
193
|
-
.where(
|
|
194
|
-
and(
|
|
195
|
-
eq(eventConsumerStateTable.name, mspName),
|
|
196
|
-
eq(eventConsumerStateTable.instanceId, SHARED_INSTANCE_SENTINEL),
|
|
197
|
-
),
|
|
198
|
-
);
|
|
169
|
+
await updateConsumerRebuildCursor(
|
|
170
|
+
tx,
|
|
171
|
+
mspName,
|
|
172
|
+
SHARED_INSTANCE_SENTINEL,
|
|
173
|
+
lastProcessedEventId,
|
|
174
|
+
);
|
|
199
175
|
});
|
|
200
176
|
} catch (e) {
|
|
201
177
|
const message = e instanceof Error ? e.message : String(e);
|
|
202
|
-
await db
|
|
203
|
-
.update(eventConsumerStateTable)
|
|
204
|
-
.set({ status: "dead", lastError: message, updatedAt: sql`now()` })
|
|
205
|
-
.where(
|
|
206
|
-
and(
|
|
207
|
-
eq(eventConsumerStateTable.name, mspName),
|
|
208
|
-
eq(eventConsumerStateTable.instanceId, SHARED_INSTANCE_SENTINEL),
|
|
209
|
-
),
|
|
210
|
-
);
|
|
178
|
+
await markConsumerRebuildFailed(db, mspName, SHARED_INSTANCE_SENTINEL, message);
|
|
211
179
|
if (deps.meter) {
|
|
212
180
|
emitProjectionRebuild(
|
|
213
181
|
deps.meter,
|
|
@@ -237,6 +205,14 @@ export async function rebuildMultiStreamProjection(
|
|
|
237
205
|
return result;
|
|
238
206
|
}
|
|
239
207
|
|
|
240
|
-
|
|
241
|
-
|
|
208
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
209
|
+
function getTableName(table: unknown): string {
|
|
210
|
+
if (typeof table !== "object" || table === null) {
|
|
211
|
+
throw new InternalError({ message: "msp-rebuild: msp.table is not a pgTable object" });
|
|
212
|
+
}
|
|
213
|
+
const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
214
|
+
if (typeof name !== "string") {
|
|
215
|
+
throw new InternalError({ message: "msp-rebuild: msp.table missing drizzle name symbol" });
|
|
216
|
+
}
|
|
217
|
+
return name;
|
|
242
218
|
}
|