@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Event-Store Performance — Gate A targets from the Sprint-B spike.
|
|
2
|
+
// Asserts that today's framework API holds the same latency envelope as
|
|
3
|
+
// the raw-SQL spike used as proof before the ES pivot.
|
|
4
|
+
//
|
|
5
|
+
// Targets (from docs/plans/architecture/event-sourcing-spike-1.md):
|
|
6
|
+
// - Write-Latency p99 < 30ms (append a single event)
|
|
7
|
+
// - Read-Latency p99 < 10ms (loadAggregate for a single aggregate)
|
|
8
|
+
// - Update-Latency p99 < 30ms (append with predecessor-check WHERE EXISTS)
|
|
9
|
+
// - Snapshot-Load < 50ms (1000-event aggregate, snapshot @ 900)
|
|
10
|
+
//
|
|
11
|
+
// Workload is sequential against local Docker Postgres — no network
|
|
12
|
+
// latency, single-node PG. Production deploys are slower; these numbers
|
|
13
|
+
// are the ceiling. Red test = framework regression, no slack tolerated.
|
|
14
|
+
|
|
15
|
+
import { sql } from "drizzle-orm";
|
|
16
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
17
|
+
import type { TenantId } from "../../engine/types";
|
|
18
|
+
import { createTestDb, type TestDb } from "../../stack";
|
|
19
|
+
import { generateId as uuid } from "../../utils";
|
|
20
|
+
import {
|
|
21
|
+
append,
|
|
22
|
+
createEventsTable,
|
|
23
|
+
loadAggregate,
|
|
24
|
+
loadAggregateWithSnapshot,
|
|
25
|
+
saveSnapshot,
|
|
26
|
+
} from "../index";
|
|
27
|
+
|
|
28
|
+
let testDb: TestDb;
|
|
29
|
+
const tenantId = uuid() as TenantId;
|
|
30
|
+
const userId = uuid();
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
testDb = await createTestDb();
|
|
34
|
+
await createEventsTable(testDb.db);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterAll(async () => {
|
|
38
|
+
await testDb.cleanup();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
await testDb.db.execute(
|
|
43
|
+
sql`TRUNCATE kumiko_events, kumiko_snapshots, kumiko_archived_streams RESTART IDENTITY CASCADE`,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function percentile(sorted: readonly number[], p: number): number {
|
|
48
|
+
const idx = Math.ceil(sorted.length * p) - 1;
|
|
49
|
+
return sorted[Math.max(0, Math.min(idx, sorted.length - 1))] ?? 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function measure<T>(op: () => Promise<T>): Promise<number> {
|
|
53
|
+
const start = performance.now();
|
|
54
|
+
await op();
|
|
55
|
+
return performance.now() - start;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("event-store performance — Gate A", () => {
|
|
59
|
+
test("write-latency p99 < 30ms over 200 sequential appends", async () => {
|
|
60
|
+
const samples: number[] = [];
|
|
61
|
+
|
|
62
|
+
// Warm-up — Connection-Pool + Drizzle-Prepare-Overhead
|
|
63
|
+
for (let i = 0; i < 10; i++) {
|
|
64
|
+
await append(testDb.db, {
|
|
65
|
+
aggregateId: uuid(),
|
|
66
|
+
aggregateType: "task",
|
|
67
|
+
tenantId,
|
|
68
|
+
expectedVersion: 0,
|
|
69
|
+
type: "task.created",
|
|
70
|
+
payload: { title: `warm ${i}` },
|
|
71
|
+
metadata: { userId },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < 200; i++) {
|
|
76
|
+
const aggregateId = uuid();
|
|
77
|
+
samples.push(
|
|
78
|
+
await measure(() =>
|
|
79
|
+
append(testDb.db, {
|
|
80
|
+
aggregateId,
|
|
81
|
+
aggregateType: "task",
|
|
82
|
+
tenantId,
|
|
83
|
+
expectedVersion: 0,
|
|
84
|
+
type: "task.created",
|
|
85
|
+
payload: { title: `sample ${i}` },
|
|
86
|
+
metadata: { userId },
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
samples.sort((a, b) => a - b);
|
|
93
|
+
const p50 = percentile(samples, 0.5);
|
|
94
|
+
const p99 = percentile(samples, 0.99);
|
|
95
|
+
console.log(` Write-latency: p50=${p50.toFixed(2)}ms, p99=${p99.toFixed(2)}ms (n=200)`);
|
|
96
|
+
|
|
97
|
+
expect(p99).toBeLessThan(30);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("read-latency p99 < 10ms for loadAggregate detail reads", async () => {
|
|
101
|
+
// Seed 200 single-event aggregates
|
|
102
|
+
const ids: string[] = [];
|
|
103
|
+
for (let i = 0; i < 200; i++) {
|
|
104
|
+
const aggregateId = uuid();
|
|
105
|
+
await append(testDb.db, {
|
|
106
|
+
aggregateId,
|
|
107
|
+
aggregateType: "task",
|
|
108
|
+
tenantId,
|
|
109
|
+
expectedVersion: 0,
|
|
110
|
+
type: "task.created",
|
|
111
|
+
payload: { title: `read ${i}` },
|
|
112
|
+
metadata: { userId },
|
|
113
|
+
});
|
|
114
|
+
ids.push(aggregateId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Warm-up
|
|
118
|
+
for (const id of ids.slice(0, 10)) {
|
|
119
|
+
await loadAggregate(testDb.db, id, tenantId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const samples: number[] = [];
|
|
123
|
+
for (const id of ids) {
|
|
124
|
+
samples.push(await measure(() => loadAggregate(testDb.db, id, tenantId)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
samples.sort((a, b) => a - b);
|
|
128
|
+
const p50 = percentile(samples, 0.5);
|
|
129
|
+
const p99 = percentile(samples, 0.99);
|
|
130
|
+
console.log(
|
|
131
|
+
` Read-latency: p50=${p50.toFixed(2)}ms, p99=${p99.toFixed(2)}ms (n=${ids.length})`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(p99).toBeLessThan(10);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("update-latency p99 < 30ms — exercises predecessor-check WHERE EXISTS path", async () => {
|
|
138
|
+
// Single aggregate, repeated updates — the INSERT … SELECT … WHERE EXISTS
|
|
139
|
+
// path is heavier than a simple create and adds an index lookup.
|
|
140
|
+
const aggregateId = uuid();
|
|
141
|
+
await append(testDb.db, {
|
|
142
|
+
aggregateId,
|
|
143
|
+
aggregateType: "task",
|
|
144
|
+
tenantId,
|
|
145
|
+
expectedVersion: 0,
|
|
146
|
+
type: "task.created",
|
|
147
|
+
payload: { title: "target" },
|
|
148
|
+
metadata: { userId },
|
|
149
|
+
});
|
|
150
|
+
let version = 1;
|
|
151
|
+
|
|
152
|
+
// Warm-up
|
|
153
|
+
for (let i = 0; i < 10; i++) {
|
|
154
|
+
await append(testDb.db, {
|
|
155
|
+
aggregateId,
|
|
156
|
+
aggregateType: "task",
|
|
157
|
+
tenantId,
|
|
158
|
+
expectedVersion: version,
|
|
159
|
+
type: "task.updated",
|
|
160
|
+
payload: { title: `warm ${i}` },
|
|
161
|
+
metadata: { userId },
|
|
162
|
+
});
|
|
163
|
+
version++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const samples: number[] = [];
|
|
167
|
+
for (let i = 0; i < 200; i++) {
|
|
168
|
+
const current = version;
|
|
169
|
+
samples.push(
|
|
170
|
+
await measure(() =>
|
|
171
|
+
append(testDb.db, {
|
|
172
|
+
aggregateId,
|
|
173
|
+
aggregateType: "task",
|
|
174
|
+
tenantId,
|
|
175
|
+
expectedVersion: current,
|
|
176
|
+
type: "task.updated",
|
|
177
|
+
payload: { title: `sample ${i}` },
|
|
178
|
+
metadata: { userId },
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
version++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
samples.sort((a, b) => a - b);
|
|
186
|
+
const p50 = percentile(samples, 0.5);
|
|
187
|
+
const p99 = percentile(samples, 0.99);
|
|
188
|
+
console.log(` Update-latency: p50=${p50.toFixed(2)}ms, p99=${p99.toFixed(2)}ms (n=200)`);
|
|
189
|
+
|
|
190
|
+
expect(p99).toBeLessThan(30);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("snapshot-load < 50ms for 1000-event aggregate (Gate A)", async () => {
|
|
194
|
+
// Bulk-seed 1000 events via direct SQL insert — 1000 sequential
|
|
195
|
+
// append() calls would take minutes. We measure the
|
|
196
|
+
// loadAggregateWithSnapshot performance on a finished stream, not
|
|
197
|
+
// the seed phase.
|
|
198
|
+
const aggregateId = uuid();
|
|
199
|
+
await testDb.db.execute(sql`
|
|
200
|
+
INSERT INTO kumiko_events (aggregate_id, aggregate_type, tenant_id, version, type, payload, metadata, created_by)
|
|
201
|
+
SELECT ${aggregateId}::uuid, 'task', ${tenantId}::uuid, 1, 'task.created',
|
|
202
|
+
jsonb_build_object('title', 'v1'),
|
|
203
|
+
jsonb_build_object('userId', ${userId}::text),
|
|
204
|
+
${userId}::text;
|
|
205
|
+
`);
|
|
206
|
+
await testDb.db.execute(sql`
|
|
207
|
+
INSERT INTO kumiko_events (aggregate_id, aggregate_type, tenant_id, version, type, payload, metadata, created_by)
|
|
208
|
+
SELECT ${aggregateId}::uuid, 'task', ${tenantId}::uuid, gs.v, 'task.updated',
|
|
209
|
+
jsonb_build_object('title', 'v' || gs.v),
|
|
210
|
+
jsonb_build_object('userId', ${userId}::text),
|
|
211
|
+
${userId}::text
|
|
212
|
+
FROM generate_series(2, 1000) gs(v);
|
|
213
|
+
`);
|
|
214
|
+
|
|
215
|
+
// Snapshot @ version 900 — typische Policy: snapshot every N events
|
|
216
|
+
await saveSnapshot(testDb.db, {
|
|
217
|
+
aggregateId,
|
|
218
|
+
tenantId,
|
|
219
|
+
aggregateType: "task",
|
|
220
|
+
version: 900,
|
|
221
|
+
state: { title: "v900", version: 900 },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
type TaskState = { title: string; version: number };
|
|
225
|
+
const reducer = (state: TaskState, evt: { payload: Record<string, unknown> }): TaskState => ({
|
|
226
|
+
...state,
|
|
227
|
+
title: (evt.payload["title"] as string) ?? state.title,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Warm-up
|
|
231
|
+
await loadAggregateWithSnapshot<TaskState>(testDb.db, aggregateId, tenantId, reducer, {
|
|
232
|
+
title: "",
|
|
233
|
+
version: 0,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const start = performance.now();
|
|
237
|
+
const result = await loadAggregateWithSnapshot<TaskState>(
|
|
238
|
+
testDb.db,
|
|
239
|
+
aggregateId,
|
|
240
|
+
tenantId,
|
|
241
|
+
reducer,
|
|
242
|
+
{ title: "", version: 0 },
|
|
243
|
+
);
|
|
244
|
+
const durationMs = performance.now() - start;
|
|
245
|
+
|
|
246
|
+
expect(result.snapshotHit).toBe(true);
|
|
247
|
+
expect(result.version).toBe(1000);
|
|
248
|
+
expect(result.state.title).toBe("v1000");
|
|
249
|
+
console.log(
|
|
250
|
+
` Snapshot-load (1000-event aggregate, 100 delta events): ${durationMs.toFixed(1)}ms`,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(durationMs).toBeLessThan(50);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Sprint E.3 — Snapshot store.
|
|
2
|
+
//
|
|
3
|
+
// Pins three invariants for the framework-internal snapshot surface:
|
|
4
|
+
// 1. saveSnapshot + loadLatestSnapshot round-trip a state.
|
|
5
|
+
// 2. loadAggregateWithSnapshot(snapshot + deltas) yields the same final
|
|
6
|
+
// state as loadAggregate + full-replay — snapshots stay truthful to
|
|
7
|
+
// the event log they compress.
|
|
8
|
+
// 3. Performance — a 1000-event aggregate with a snapshot at v900 loads
|
|
9
|
+
// in < 50ms (typical asOf/reducer rehydrate budget). Same gate the
|
|
10
|
+
// spike proved on raw SQL, now enforced on the framework path.
|
|
11
|
+
|
|
12
|
+
import { sql } from "drizzle-orm";
|
|
13
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
14
|
+
import type { TenantId } from "../../engine/types";
|
|
15
|
+
import { createTestDb, type TestDb } from "../../stack";
|
|
16
|
+
import { generateId as uuid } from "../../utils";
|
|
17
|
+
import {
|
|
18
|
+
append,
|
|
19
|
+
archiveStream,
|
|
20
|
+
createEventsTable,
|
|
21
|
+
loadAggregate,
|
|
22
|
+
loadAggregateWithSnapshot,
|
|
23
|
+
loadLatestSnapshot,
|
|
24
|
+
type SnapshotReducer,
|
|
25
|
+
saveSnapshot,
|
|
26
|
+
} from "../index";
|
|
27
|
+
|
|
28
|
+
let testDb: TestDb;
|
|
29
|
+
const tenant = uuid() as TenantId;
|
|
30
|
+
const userId = uuid();
|
|
31
|
+
|
|
32
|
+
type CounterState = Record<string, unknown> & {
|
|
33
|
+
readonly count: number;
|
|
34
|
+
readonly label: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const initial: CounterState = { count: 0, label: "init" };
|
|
38
|
+
|
|
39
|
+
// Tiny reducer: count.incremented → count++, label.set → overwrite label.
|
|
40
|
+
const reducer: SnapshotReducer<CounterState> = (state, event) => {
|
|
41
|
+
if (event.type === "counter.incremented") {
|
|
42
|
+
const by = (event.payload["by"] as number | undefined) ?? 1;
|
|
43
|
+
return { ...state, count: state.count + by };
|
|
44
|
+
}
|
|
45
|
+
if (event.type === "counter.label-set") {
|
|
46
|
+
return { ...state, label: event.payload["label"] as string };
|
|
47
|
+
}
|
|
48
|
+
return state;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
testDb = await createTestDb();
|
|
53
|
+
await createEventsTable(testDb.db);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(async () => {
|
|
57
|
+
await testDb.cleanup();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
await testDb.db.execute(
|
|
62
|
+
sql`TRUNCATE kumiko_events, kumiko_snapshots, kumiko_archived_streams RESTART IDENTITY`,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Append N increment events to a fresh aggregate. Returns the aggregate id.
|
|
67
|
+
async function seedAggregate(eventCount: number): Promise<string> {
|
|
68
|
+
const aggId = uuid();
|
|
69
|
+
for (let i = 0; i < eventCount; i++) {
|
|
70
|
+
await append(testDb.db, {
|
|
71
|
+
aggregateId: aggId,
|
|
72
|
+
aggregateType: "counter",
|
|
73
|
+
tenantId: tenant,
|
|
74
|
+
expectedVersion: i,
|
|
75
|
+
type: "counter.incremented",
|
|
76
|
+
payload: { by: 1 },
|
|
77
|
+
metadata: { userId },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return aggId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reduce by loading every event through the framework's loadAggregate —
|
|
84
|
+
// the ground truth the snapshot path must match. Using the real API (not
|
|
85
|
+
// raw SQL) keeps the invariant honest: if loadAggregate semantics drift,
|
|
86
|
+
// this helper shifts with them.
|
|
87
|
+
async function loadFullState(aggregateId: string): Promise<CounterState> {
|
|
88
|
+
const events = await loadAggregate(testDb.db, aggregateId, tenant);
|
|
89
|
+
let state: CounterState = initial;
|
|
90
|
+
for (const event of events) {
|
|
91
|
+
state = reducer(state, event);
|
|
92
|
+
}
|
|
93
|
+
return state;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("snapshot-store — round-trip", () => {
|
|
97
|
+
test("saveSnapshot + loadLatestSnapshot roundtrip the state", async () => {
|
|
98
|
+
const aggId = uuid();
|
|
99
|
+
await saveSnapshot(testDb.db, {
|
|
100
|
+
aggregateId: aggId,
|
|
101
|
+
tenantId: tenant,
|
|
102
|
+
aggregateType: "counter",
|
|
103
|
+
version: 42,
|
|
104
|
+
state: { count: 100, label: "checkpoint" },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const loaded = await loadLatestSnapshot<CounterState>(testDb.db, aggId, tenant);
|
|
108
|
+
expect(loaded).not.toBeNull();
|
|
109
|
+
expect(loaded?.version).toBe(42);
|
|
110
|
+
expect(loaded?.state).toEqual({ count: 100, label: "checkpoint" });
|
|
111
|
+
expect(loaded?.aggregateType).toBe("counter");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("saveSnapshot is idempotent — re-snapshotting the same version upserts", async () => {
|
|
115
|
+
const aggId = uuid();
|
|
116
|
+
await saveSnapshot(testDb.db, {
|
|
117
|
+
aggregateId: aggId,
|
|
118
|
+
tenantId: tenant,
|
|
119
|
+
aggregateType: "counter",
|
|
120
|
+
version: 10,
|
|
121
|
+
state: { count: 10, label: "v1" },
|
|
122
|
+
});
|
|
123
|
+
await saveSnapshot(testDb.db, {
|
|
124
|
+
aggregateId: aggId,
|
|
125
|
+
tenantId: tenant,
|
|
126
|
+
aggregateType: "counter",
|
|
127
|
+
version: 10,
|
|
128
|
+
state: { count: 10, label: "v2-updated" },
|
|
129
|
+
});
|
|
130
|
+
const loaded = await loadLatestSnapshot<CounterState>(testDb.db, aggId, tenant);
|
|
131
|
+
expect(loaded?.state.label).toBe("v2-updated");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("loadLatestSnapshot picks the highest version when multiple exist", async () => {
|
|
135
|
+
const aggId = uuid();
|
|
136
|
+
for (const v of [5, 50, 20, 100, 75]) {
|
|
137
|
+
await saveSnapshot(testDb.db, {
|
|
138
|
+
aggregateId: aggId,
|
|
139
|
+
tenantId: tenant,
|
|
140
|
+
aggregateType: "counter",
|
|
141
|
+
version: v,
|
|
142
|
+
state: { count: v * 2, label: `at-${v}` },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const loaded = await loadLatestSnapshot<CounterState>(testDb.db, aggId, tenant);
|
|
146
|
+
expect(loaded?.version).toBe(100);
|
|
147
|
+
expect(loaded?.state.label).toBe("at-100");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("snapshot-store — loadAggregateWithSnapshot", () => {
|
|
152
|
+
test("snapshot + delta replay equals full replay", async () => {
|
|
153
|
+
const aggId = await seedAggregate(50);
|
|
154
|
+
// Seeded aggregate has 50 increment events — state at v40 is count=40.
|
|
155
|
+
// Snapshot MUST reflect that truth; otherwise the "snapshot + deltas =
|
|
156
|
+
// full replay" invariant is meaningless.
|
|
157
|
+
const partial = { count: 40, label: "snap-at-40" };
|
|
158
|
+
await saveSnapshot(testDb.db, {
|
|
159
|
+
aggregateId: aggId,
|
|
160
|
+
tenantId: tenant,
|
|
161
|
+
aggregateType: "counter",
|
|
162
|
+
version: 40,
|
|
163
|
+
state: partial,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const full = await loadFullState(aggId);
|
|
167
|
+
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
168
|
+
testDb.db,
|
|
169
|
+
aggId,
|
|
170
|
+
tenant,
|
|
171
|
+
reducer,
|
|
172
|
+
initial,
|
|
173
|
+
);
|
|
174
|
+
expect(snapBased.snapshotHit).toBe(true);
|
|
175
|
+
expect(snapBased.version).toBe(50);
|
|
176
|
+
expect(snapBased.state.count).toBe(full.count);
|
|
177
|
+
expect(snapBased.state.count).toBe(50);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("no snapshot → falls back to full replay (snapshotHit=false)", async () => {
|
|
181
|
+
const aggId = await seedAggregate(20);
|
|
182
|
+
const full = await loadFullState(aggId);
|
|
183
|
+
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
184
|
+
testDb.db,
|
|
185
|
+
aggId,
|
|
186
|
+
tenant,
|
|
187
|
+
reducer,
|
|
188
|
+
initial,
|
|
189
|
+
);
|
|
190
|
+
expect(snapBased.snapshotHit).toBe(false);
|
|
191
|
+
expect(snapBased.version).toBe(20);
|
|
192
|
+
expect(snapBased.state.count).toBe(full.count);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("archived stream returns initial + snapshotHit=false (matches loadAggregate)", async () => {
|
|
196
|
+
// Archive semantics must be symmetric with loadAggregate — an archived
|
|
197
|
+
// stream is "gone" from the default read path, regardless of snapshot
|
|
198
|
+
// presence. Otherwise a snapshot would silently survive a GDPR-style
|
|
199
|
+
// archival and leak state that the event log hid.
|
|
200
|
+
const aggId = await seedAggregate(10);
|
|
201
|
+
await saveSnapshot(testDb.db, {
|
|
202
|
+
aggregateId: aggId,
|
|
203
|
+
tenantId: tenant,
|
|
204
|
+
aggregateType: "counter",
|
|
205
|
+
version: 10,
|
|
206
|
+
state: { count: 10, label: "pre-archive" },
|
|
207
|
+
});
|
|
208
|
+
await archiveStream(testDb.db, {
|
|
209
|
+
tenantId: tenant,
|
|
210
|
+
aggregateId: aggId,
|
|
211
|
+
aggregateType: "counter",
|
|
212
|
+
archivedBy: userId,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
216
|
+
testDb.db,
|
|
217
|
+
aggId,
|
|
218
|
+
tenant,
|
|
219
|
+
reducer,
|
|
220
|
+
initial,
|
|
221
|
+
);
|
|
222
|
+
expect(snapBased.state).toEqual(initial);
|
|
223
|
+
expect(snapBased.version).toBe(0);
|
|
224
|
+
expect(snapBased.snapshotHit).toBe(false);
|
|
225
|
+
|
|
226
|
+
// includeArchived opt-in surfaces the snapshot + deltas for ops tooling.
|
|
227
|
+
const archived = await loadAggregateWithSnapshot<CounterState>(
|
|
228
|
+
testDb.db,
|
|
229
|
+
aggId,
|
|
230
|
+
tenant,
|
|
231
|
+
reducer,
|
|
232
|
+
initial,
|
|
233
|
+
{ includeArchived: true },
|
|
234
|
+
);
|
|
235
|
+
expect(archived.snapshotHit).toBe(true);
|
|
236
|
+
expect(archived.state.count).toBe(10);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("1000-event aggregate with snapshot at v900 loads in under 50ms", async () => {
|
|
240
|
+
const aggId = await seedAggregate(1000);
|
|
241
|
+
await saveSnapshot(testDb.db, {
|
|
242
|
+
aggregateId: aggId,
|
|
243
|
+
tenantId: tenant,
|
|
244
|
+
aggregateType: "counter",
|
|
245
|
+
version: 900,
|
|
246
|
+
state: { count: 900, label: "snap-at-900" },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Warm cache
|
|
250
|
+
await loadAggregateWithSnapshot<CounterState>(testDb.db, aggId, tenant, reducer, initial);
|
|
251
|
+
|
|
252
|
+
const start = performance.now();
|
|
253
|
+
const snapBased = await loadAggregateWithSnapshot<CounterState>(
|
|
254
|
+
testDb.db,
|
|
255
|
+
aggId,
|
|
256
|
+
tenant,
|
|
257
|
+
reducer,
|
|
258
|
+
initial,
|
|
259
|
+
);
|
|
260
|
+
const elapsedMs = performance.now() - start;
|
|
261
|
+
|
|
262
|
+
expect(snapBased.snapshotHit).toBe(true);
|
|
263
|
+
expect(snapBased.version).toBe(1000);
|
|
264
|
+
expect(snapBased.state.count).toBe(1000);
|
|
265
|
+
expect(elapsedMs).toBeLessThan(50);
|
|
266
|
+
});
|
|
267
|
+
});
|