@spine-event-engine/server 2.0.0-snapshot.2
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/LICENSE +201 -0
- package/README.md +569 -0
- package/REFERENCE.md +488 -0
- package/dist/bus/command-bus.d.ts +61 -0
- package/dist/bus/command-bus.d.ts.map +1 -0
- package/dist/bus/command-bus.js +201 -0
- package/dist/bus/command-bus.js.map +1 -0
- package/dist/bus/command-dispatcher-registry.d.ts +33 -0
- package/dist/bus/command-dispatcher-registry.d.ts.map +1 -0
- package/dist/bus/command-dispatcher-registry.js +75 -0
- package/dist/bus/command-dispatcher-registry.js.map +1 -0
- package/dist/bus/command-dispatcher.d.ts +25 -0
- package/dist/bus/command-dispatcher.d.ts.map +1 -0
- package/dist/bus/command-dispatcher.js +15 -0
- package/dist/bus/command-dispatcher.js.map +1 -0
- package/dist/bus/command-errors.d.ts +23 -0
- package/dist/bus/command-errors.d.ts.map +1 -0
- package/dist/bus/command-errors.js +52 -0
- package/dist/bus/command-errors.js.map +1 -0
- package/dist/bus/event-bus.d.ts +113 -0
- package/dist/bus/event-bus.d.ts.map +1 -0
- package/dist/bus/event-bus.js +518 -0
- package/dist/bus/event-bus.js.map +1 -0
- package/dist/bus/event-dispatcher-origin-schemas.d.ts +17 -0
- package/dist/bus/event-dispatcher-origin-schemas.d.ts.map +1 -0
- package/dist/bus/event-dispatcher-origin-schemas.js +32 -0
- package/dist/bus/event-dispatcher-origin-schemas.js.map +1 -0
- package/dist/bus/event-dispatcher-registry.d.ts +48 -0
- package/dist/bus/event-dispatcher-registry.d.ts.map +1 -0
- package/dist/bus/event-dispatcher-registry.js +176 -0
- package/dist/bus/event-dispatcher-registry.js.map +1 -0
- package/dist/bus/event-dispatcher.d.ts +48 -0
- package/dist/bus/event-dispatcher.d.ts.map +1 -0
- package/dist/bus/event-dispatcher.js +15 -0
- package/dist/bus/event-dispatcher.js.map +1 -0
- package/dist/context/bounded-context.d.ts +606 -0
- package/dist/context/bounded-context.d.ts.map +1 -0
- package/dist/context/bounded-context.js +1780 -0
- package/dist/context/bounded-context.js.map +1 -0
- package/dist/context/entity-inbox.d.ts +63 -0
- package/dist/context/entity-inbox.d.ts.map +1 -0
- package/dist/context/entity-inbox.js +380 -0
- package/dist/context/entity-inbox.js.map +1 -0
- package/dist/context/local-inbox-handoff.d.ts +244 -0
- package/dist/context/local-inbox-handoff.d.ts.map +1 -0
- package/dist/context/local-inbox-handoff.js +334 -0
- package/dist/context/local-inbox-handoff.js.map +1 -0
- package/dist/context/projection-handoff.d.ts +46 -0
- package/dist/context/projection-handoff.d.ts.map +1 -0
- package/dist/context/projection-handoff.js +172 -0
- package/dist/context/projection-handoff.js.map +1 -0
- package/dist/context/tenant-index.d.ts +41 -0
- package/dist/context/tenant-index.d.ts.map +1 -0
- package/dist/context/tenant-index.js +92 -0
- package/dist/context/tenant-index.js.map +1 -0
- package/dist/delivery/conditional-pickup.d.ts +25 -0
- package/dist/delivery/conditional-pickup.d.ts.map +1 -0
- package/dist/delivery/conditional-pickup.js +26 -0
- package/dist/delivery/conditional-pickup.js.map +1 -0
- package/dist/delivery/delivery-builder.d.ts +216 -0
- package/dist/delivery/delivery-builder.d.ts.map +1 -0
- package/dist/delivery/delivery-builder.js +323 -0
- package/dist/delivery/delivery-builder.js.map +1 -0
- package/dist/delivery/delivery-lease.d.ts +15 -0
- package/dist/delivery/delivery-lease.d.ts.map +1 -0
- package/dist/delivery/delivery-lease.js +44 -0
- package/dist/delivery/delivery-lease.js.map +1 -0
- package/dist/delivery/delivery-loop.d.ts +113 -0
- package/dist/delivery/delivery-loop.d.ts.map +1 -0
- package/dist/delivery/delivery-loop.js +119 -0
- package/dist/delivery/delivery-loop.js.map +1 -0
- package/dist/delivery/delivery-monitor.d.ts +165 -0
- package/dist/delivery/delivery-monitor.d.ts.map +1 -0
- package/dist/delivery/delivery-monitor.js +168 -0
- package/dist/delivery/delivery-monitor.js.map +1 -0
- package/dist/delivery/delivery-ports.d.ts +141 -0
- package/dist/delivery/delivery-ports.d.ts.map +1 -0
- package/dist/delivery/delivery-ports.js +15 -0
- package/dist/delivery/delivery-ports.js.map +1 -0
- package/dist/delivery/delivery-run-control.d.ts +50 -0
- package/dist/delivery/delivery-run-control.d.ts.map +1 -0
- package/dist/delivery/delivery-run-control.js +66 -0
- package/dist/delivery/delivery-run-control.js.map +1 -0
- package/dist/delivery/delivery-run-coordinator.d.ts +218 -0
- package/dist/delivery/delivery-run-coordinator.d.ts.map +1 -0
- package/dist/delivery/delivery-run-coordinator.js +609 -0
- package/dist/delivery/delivery-run-coordinator.js.map +1 -0
- package/dist/delivery/delivery-storage-error.d.ts +13 -0
- package/dist/delivery/delivery-storage-error.d.ts.map +1 -0
- package/dist/delivery/delivery-storage-error.js +30 -0
- package/dist/delivery/delivery-storage-error.js.map +1 -0
- package/dist/delivery/delivery-supervisor.d.ts +175 -0
- package/dist/delivery/delivery-supervisor.d.ts.map +1 -0
- package/dist/delivery/delivery-supervisor.js +469 -0
- package/dist/delivery/delivery-supervisor.js.map +1 -0
- package/dist/delivery/delivery-worker.d.ts +180 -0
- package/dist/delivery/delivery-worker.d.ts.map +1 -0
- package/dist/delivery/delivery-worker.js +232 -0
- package/dist/delivery/delivery-worker.js.map +1 -0
- package/dist/delivery/delivery.d.ts +238 -0
- package/dist/delivery/delivery.d.ts.map +1 -0
- package/dist/delivery/delivery.js +400 -0
- package/dist/delivery/delivery.js.map +1 -0
- package/dist/delivery/inbox-records.d.ts +19 -0
- package/dist/delivery/inbox-records.d.ts.map +1 -0
- package/dist/delivery/inbox-records.js +291 -0
- package/dist/delivery/inbox-records.js.map +1 -0
- package/dist/delivery/inbox-storage.d.ts +81 -0
- package/dist/delivery/inbox-storage.d.ts.map +1 -0
- package/dist/delivery/inbox-storage.js +353 -0
- package/dist/delivery/inbox-storage.js.map +1 -0
- package/dist/delivery/inbox.d.ts +263 -0
- package/dist/delivery/inbox.d.ts.map +1 -0
- package/dist/delivery/inbox.js +172 -0
- package/dist/delivery/inbox.js.map +1 -0
- package/dist/delivery/parked-delivery-obligations.d.ts +182 -0
- package/dist/delivery/parked-delivery-obligations.d.ts.map +1 -0
- package/dist/delivery/parked-delivery-obligations.js +390 -0
- package/dist/delivery/parked-delivery-obligations.js.map +1 -0
- package/dist/delivery/shard-index.d.ts +41 -0
- package/dist/delivery/shard-index.d.ts.map +1 -0
- package/dist/delivery/shard-index.js +67 -0
- package/dist/delivery/shard-index.js.map +1 -0
- package/dist/delivery/sharded-work-registry.d.ts +114 -0
- package/dist/delivery/sharded-work-registry.d.ts.map +1 -0
- package/dist/delivery/sharded-work-registry.js +360 -0
- package/dist/delivery/sharded-work-registry.js.map +1 -0
- package/dist/entity/entity-metadata.d.ts +173 -0
- package/dist/entity/entity-metadata.d.ts.map +1 -0
- package/dist/entity/entity-metadata.js +187 -0
- package/dist/entity/entity-metadata.js.map +1 -0
- package/dist/entity/entity-storage-descriptor.d.ts +64 -0
- package/dist/entity/entity-storage-descriptor.d.ts.map +1 -0
- package/dist/entity/entity-storage-descriptor.js +227 -0
- package/dist/entity/entity-storage-descriptor.js.map +1 -0
- package/dist/entity/entity-transaction.d.ts +372 -0
- package/dist/entity/entity-transaction.d.ts.map +1 -0
- package/dist/entity/entity-transaction.js +413 -0
- package/dist/entity/entity-transaction.js.map +1 -0
- package/dist/entity/entity-transition-validation.d.ts +47 -0
- package/dist/entity/entity-transition-validation.d.ts.map +1 -0
- package/dist/entity/entity-transition-validation.js +327 -0
- package/dist/entity/entity-transition-validation.js.map +1 -0
- package/dist/entity/entity.d.ts +499 -0
- package/dist/entity/entity.d.ts.map +1 -0
- package/dist/entity/entity.js +829 -0
- package/dist/entity/entity.js.map +1 -0
- package/dist/entity/implicit-required-id.d.ts +15 -0
- package/dist/entity/implicit-required-id.d.ts.map +1 -0
- package/dist/entity/implicit-required-id.js +123 -0
- package/dist/entity/implicit-required-id.js.map +1 -0
- package/dist/entity/spec-scanner.d.ts +14 -0
- package/dist/entity/spec-scanner.d.ts.map +1 -0
- package/dist/entity/spec-scanner.js +37 -0
- package/dist/entity/spec-scanner.js.map +1 -0
- package/dist/handler/build-time-handler-analyzer.d.ts +150 -0
- package/dist/handler/build-time-handler-analyzer.d.ts.map +1 -0
- package/dist/handler/build-time-handler-analyzer.js +1101 -0
- package/dist/handler/build-time-handler-analyzer.js.map +1 -0
- package/dist/handler/command-registration-readiness.d.ts +99 -0
- package/dist/handler/command-registration-readiness.d.ts.map +1 -0
- package/dist/handler/command-registration-readiness.js +118 -0
- package/dist/handler/command-registration-readiness.js.map +1 -0
- package/dist/handler/event-handler-filter.d.ts +43 -0
- package/dist/handler/event-handler-filter.d.ts.map +1 -0
- package/dist/handler/event-handler-filter.js +128 -0
- package/dist/handler/event-handler-filter.js.map +1 -0
- package/dist/handler/event-registration-readiness.d.ts +196 -0
- package/dist/handler/event-registration-readiness.d.ts.map +1 -0
- package/dist/handler/event-registration-readiness.js +187 -0
- package/dist/handler/event-registration-readiness.js.map +1 -0
- package/dist/handler/external.d.ts +10 -0
- package/dist/handler/external.d.ts.map +1 -0
- package/dist/handler/external.js +15 -0
- package/dist/handler/external.js.map +1 -0
- package/dist/handler/generated-handler-registry.d.ts +154 -0
- package/dist/handler/generated-handler-registry.d.ts.map +1 -0
- package/dist/handler/generated-handler-registry.js +223 -0
- package/dist/handler/generated-handler-registry.js.map +1 -0
- package/dist/handler/generated-registry-discovery.d.ts +94 -0
- package/dist/handler/generated-registry-discovery.d.ts.map +1 -0
- package/dist/handler/generated-registry-discovery.js +228 -0
- package/dist/handler/generated-registry-discovery.js.map +1 -0
- package/dist/handler/generated-registry-writer.d.ts +58 -0
- package/dist/handler/generated-registry-writer.d.ts.map +1 -0
- package/dist/handler/generated-registry-writer.js +458 -0
- package/dist/handler/generated-registry-writer.js.map +1 -0
- package/dist/handler/handler-codegen.d.ts +48 -0
- package/dist/handler/handler-codegen.d.ts.map +1 -0
- package/dist/handler/handler-codegen.js +214 -0
- package/dist/handler/handler-codegen.js.map +1 -0
- package/dist/handler/handler-decorators.d.ts +134 -0
- package/dist/handler/handler-decorators.d.ts.map +1 -0
- package/dist/handler/handler-decorators.js +239 -0
- package/dist/handler/handler-decorators.js.map +1 -0
- package/dist/handler/handler-metadata.d.ts +510 -0
- package/dist/handler/handler-metadata.d.ts.map +1 -0
- package/dist/handler/handler-metadata.js +391 -0
- package/dist/handler/handler-metadata.js.map +1 -0
- package/dist/handler/registration-readiness-metadata.d.ts +68 -0
- package/dist/handler/registration-readiness-metadata.d.ts.map +1 -0
- package/dist/handler/registration-readiness-metadata.js +170 -0
- package/dist/handler/registration-readiness-metadata.js.map +1 -0
- package/dist/handler/rejection-source.d.ts +11 -0
- package/dist/handler/rejection-source.d.ts.map +1 -0
- package/dist/handler/rejection-source.js +25 -0
- package/dist/handler/rejection-source.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/external-messages.d.ts +49 -0
- package/dist/integration/external-messages.d.ts.map +1 -0
- package/dist/integration/external-messages.js +112 -0
- package/dist/integration/external-messages.js.map +1 -0
- package/dist/integration/integration-broker.d.ts +74 -0
- package/dist/integration/integration-broker.d.ts.map +1 -0
- package/dist/integration/integration-broker.js +444 -0
- package/dist/integration/integration-broker.js.map +1 -0
- package/dist/integration/third-party-context.d.ts +56 -0
- package/dist/integration/third-party-context.d.ts.map +1 -0
- package/dist/integration/third-party-context.js +130 -0
- package/dist/integration/third-party-context.js.map +1 -0
- package/dist/repository/command-errors.d.ts +25 -0
- package/dist/repository/command-errors.d.ts.map +1 -0
- package/dist/repository/command-errors.js +43 -0
- package/dist/repository/command-errors.js.map +1 -0
- package/dist/repository/command-routing.d.ts +72 -0
- package/dist/repository/command-routing.d.ts.map +1 -0
- package/dist/repository/command-routing.js +98 -0
- package/dist/repository/command-routing.js.map +1 -0
- package/dist/repository/commit-fence.d.ts +22 -0
- package/dist/repository/commit-fence.d.ts.map +1 -0
- package/dist/repository/commit-fence.js +37 -0
- package/dist/repository/commit-fence.js.map +1 -0
- package/dist/repository/event-routing.d.ts +71 -0
- package/dist/repository/event-routing.d.ts.map +1 -0
- package/dist/repository/event-routing.js +96 -0
- package/dist/repository/event-routing.js.map +1 -0
- package/dist/repository/primitive-id.d.ts +86 -0
- package/dist/repository/primitive-id.d.ts.map +1 -0
- package/dist/repository/primitive-id.js +110 -0
- package/dist/repository/primitive-id.js.map +1 -0
- package/dist/repository/repository.d.ts +670 -0
- package/dist/repository/repository.d.ts.map +1 -0
- package/dist/repository/repository.js +3298 -0
- package/dist/repository/repository.js.map +1 -0
- package/dist/repository/routing-declarations.d.ts +75 -0
- package/dist/repository/routing-declarations.d.ts.map +1 -0
- package/dist/repository/routing-declarations.js +101 -0
- package/dist/repository/routing-declarations.js.map +1 -0
- package/dist/repository/state-update-routing.d.ts +66 -0
- package/dist/repository/state-update-routing.d.ts.map +1 -0
- package/dist/repository/state-update-routing.js +91 -0
- package/dist/repository/state-update-routing.js.map +1 -0
- package/dist/runtime/runtime.d.ts +156 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +215 -0
- package/dist/runtime/runtime.js.map +1 -0
- package/dist/runtime/signal-intake.d.ts +84 -0
- package/dist/runtime/signal-intake.d.ts.map +1 -0
- package/dist/runtime/signal-intake.js +94 -0
- package/dist/runtime/signal-intake.js.map +1 -0
- package/dist/runtime/signal-metadata.d.ts +242 -0
- package/dist/runtime/signal-metadata.d.ts.map +1 -0
- package/dist/runtime/signal-metadata.js +368 -0
- package/dist/runtime/signal-metadata.js.map +1 -0
- package/dist/server/browser-server.d.ts +49 -0
- package/dist/server/browser-server.d.ts.map +1 -0
- package/dist/server/browser-server.js +633 -0
- package/dist/server/browser-server.js.map +1 -0
- package/dist/server/durable-subscription-bindings.d.ts +122 -0
- package/dist/server/durable-subscription-bindings.d.ts.map +1 -0
- package/dist/server/durable-subscription-bindings.js +524 -0
- package/dist/server/durable-subscription-bindings.js.map +1 -0
- package/dist/server/environment-attachment.d.ts +356 -0
- package/dist/server/environment-attachment.d.ts.map +1 -0
- package/dist/server/environment-attachment.js +2061 -0
- package/dist/server/environment-attachment.js.map +1 -0
- package/dist/server/environment-delivery-records.d.ts +77 -0
- package/dist/server/environment-delivery-records.d.ts.map +1 -0
- package/dist/server/environment-delivery-records.js +282 -0
- package/dist/server/environment-delivery-records.js.map +1 -0
- package/dist/server/environment-delivery-worker.d.ts +161 -0
- package/dist/server/environment-delivery-worker.d.ts.map +1 -0
- package/dist/server/environment-delivery-worker.js +664 -0
- package/dist/server/environment-delivery-worker.js.map +1 -0
- package/dist/server/environment.d.ts +43 -0
- package/dist/server/environment.d.ts.map +1 -0
- package/dist/server/environment.js +90 -0
- package/dist/server/environment.js.map +1 -0
- package/dist/server/managed-child-subscription.d.ts +11 -0
- package/dist/server/managed-child-subscription.d.ts.map +1 -0
- package/dist/server/managed-child-subscription.js +44 -0
- package/dist/server/managed-child-subscription.js.map +1 -0
- package/dist/server/managed-server-application.d.ts +324 -0
- package/dist/server/managed-server-application.d.ts.map +1 -0
- package/dist/server/managed-server-application.js +937 -0
- package/dist/server/managed-server-application.js.map +1 -0
- package/dist/server/node-coordinator.d.ts +191 -0
- package/dist/server/node-coordinator.d.ts.map +1 -0
- package/dist/server/node-coordinator.js +552 -0
- package/dist/server/node-coordinator.js.map +1 -0
- package/dist/server/process-server-coordinator.d.ts +15 -0
- package/dist/server/process-server-coordinator.d.ts.map +1 -0
- package/dist/server/process-server-coordinator.js +112 -0
- package/dist/server/process-server-coordinator.js.map +1 -0
- package/dist/server/reserved-spine-rpc-paths.json +8 -0
- package/dist/server/retryable-close.d.ts +28 -0
- package/dist/server/retryable-close.d.ts.map +1 -0
- package/dist/server/retryable-close.js +129 -0
- package/dist/server/retryable-close.js.map +1 -0
- package/dist/server/server-environment.d.ts +180 -0
- package/dist/server/server-environment.d.ts.map +1 -0
- package/dist/server/server-environment.js +418 -0
- package/dist/server/server-environment.js.map +1 -0
- package/dist/server/server-log.d.ts +18 -0
- package/dist/server/server-log.d.ts.map +1 -0
- package/dist/server/server-log.js +96 -0
- package/dist/server/server-log.js.map +1 -0
- package/dist/server/server.d.ts +389 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +752 -0
- package/dist/server/server.js.map +1 -0
- package/dist/services/spine-services.d.ts +88 -0
- package/dist/services/spine-services.d.ts.map +1 -0
- package/dist/services/spine-services.js +1586 -0
- package/dist/services/spine-services.js.map +1 -0
- package/dist/stand/stand.d.ts +294 -0
- package/dist/stand/stand.d.ts.map +1 -0
- package/dist/stand/stand.js +680 -0
- package/dist/stand/stand.js.map +1 -0
- package/dist/stand/subscription-observer.d.ts +53 -0
- package/dist/stand/subscription-observer.d.ts.map +1 -0
- package/dist/stand/subscription-observer.js +444 -0
- package/dist/stand/subscription-observer.js.map +1 -0
- package/dist/stand/subscription-records.d.ts +12 -0
- package/dist/stand/subscription-records.d.ts.map +1 -0
- package/dist/stand/subscription-records.js +99 -0
- package/dist/stand/subscription-records.js.map +1 -0
- package/dist/stand/subscription-registry.d.ts +285 -0
- package/dist/stand/subscription-registry.d.ts.map +1 -0
- package/dist/stand/subscription-registry.js +403 -0
- package/dist/stand/subscription-registry.js.map +1 -0
- package/dist/stand/subscription-runtime.d.ts +102 -0
- package/dist/stand/subscription-runtime.d.ts.map +1 -0
- package/dist/stand/subscription-runtime.js +380 -0
- package/dist/stand/subscription-runtime.js.map +1 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +34 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,3298 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { clone, create, ScalarType, toBinary } from "@bufbuild/protobuf";
|
|
15
|
+
import { AnySchema, Int32ValueSchema, Int64ValueSchema, StringValueSchema, TimestampSchema, } from "@bufbuild/protobuf/wkt";
|
|
16
|
+
import { ValidationException, RejectionThrowable, Validate, TypeUrls, AnyMessages, Identifiers, StringifierRegistry, } from "@spine-event-engine/core";
|
|
17
|
+
import { CommandContextSchema, CommandIdSchema, CommandSchema, EventContextSchema, EventIdSchema, EventSchema, MessageIdSchema, TenantIdSchema, RejectionEventContextSchema, VersionSchema, EntityOption_Kind, } from "@spine-event-engine/proto";
|
|
18
|
+
import * as EntityLog from "@spine-event-engine/proto/generated/spine/system/server/entity_log_events_pb.js";
|
|
19
|
+
import { EntityTypeNameSchema } from "@spine-event-engine/proto/generated/spine/system/server/entity_type_pb.js";
|
|
20
|
+
import { TenantBoundary, } from "@spine-event-engine/storage";
|
|
21
|
+
import { EntityCommitStorageFactories } from "@spine-event-engine/storage/internal/entity-commit";
|
|
22
|
+
import { CommandValidationError } from "../bus/command-errors.js";
|
|
23
|
+
import { CommandRoutingInternals, } from "./command-routing.js";
|
|
24
|
+
import { EventRoutingInternals } from "./event-routing.js";
|
|
25
|
+
import { StateUpdateRoutingInternals, } from "./state-update-routing.js";
|
|
26
|
+
import { RoutingDeclarations } from "./routing-declarations.js";
|
|
27
|
+
import { EventDispatcherOriginSchemas } from "../bus/event-dispatcher-origin-schemas.js";
|
|
28
|
+
import { Delivery } from "../delivery/delivery.js";
|
|
29
|
+
import { commitFenced } from "./commit-fence.js";
|
|
30
|
+
import { InboxTargets } from "../delivery/inbox.js";
|
|
31
|
+
import { ShardIndex } from "../delivery/shard-index.js";
|
|
32
|
+
import { Aggregate, ProcessManager, Projection, entityHistoryAccess, transactionalEntityAccess, } from "../entity/entity.js";
|
|
33
|
+
import { describeEntityMetadata, attachEntitySchema, } from "../entity/entity-metadata.js";
|
|
34
|
+
import { EntityIds, EntityRecords, entityStorageDescriptor, } from "../entity/entity-storage-descriptor.js";
|
|
35
|
+
import { SpecScanner } from "../entity/spec-scanner.js";
|
|
36
|
+
import { CommandRegistrationReadiness, } from "../handler/command-registration-readiness.js";
|
|
37
|
+
import { EventRegistrationReadiness, } from "../handler/event-registration-readiness.js";
|
|
38
|
+
import { EventHandlerFilters, } from "../handler/event-handler-filter.js";
|
|
39
|
+
import { SignalMetadata } from "../runtime/signal-metadata.js";
|
|
40
|
+
import { HandlerMetadataRegistry, HandlerMetadataValues, } from "../handler/handler-metadata.js";
|
|
41
|
+
import { standAccess } from "../stand/stand.js";
|
|
42
|
+
import { TransitionValidationError } from "./command-errors.js";
|
|
43
|
+
import { MessageIds, PrimitiveIds } from "./primitive-id.js";
|
|
44
|
+
import { ImplicitRequiredIds } from "../entity/implicit-required-id.js";
|
|
45
|
+
/**
|
|
46
|
+
* Describes an error raised when repository identity metadata cannot be constructed.
|
|
47
|
+
*/
|
|
48
|
+
export class RepositoryIdentityError extends Error {
|
|
49
|
+
// prettier-ignore
|
|
50
|
+
/**
|
|
51
|
+
* Stable code for callers/tests that need structured failure handling.
|
|
52
|
+
*/
|
|
53
|
+
code;
|
|
54
|
+
/**
|
|
55
|
+
* Creates a repository identity error.
|
|
56
|
+
*
|
|
57
|
+
* @param code The stable reason for the failed identity.
|
|
58
|
+
* @param message The diagnostic message.
|
|
59
|
+
*/
|
|
60
|
+
constructor(code, message) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "RepositoryIdentityError";
|
|
63
|
+
this.code = code;
|
|
64
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Repository identity and context-owned storage registration over one entity constructor and state schema.
|
|
69
|
+
*
|
|
70
|
+
* The class records the ownership facts bounded-context registration needs for
|
|
71
|
+
* duplicate and conflict checks. Context assembly uses this metadata to attach
|
|
72
|
+
* a repository to one built context and open state record storage. With
|
|
73
|
+
* authentic explicit aggregate handler metadata, the built context can also
|
|
74
|
+
* execute aggregate commands through repository-owned assignees and live
|
|
75
|
+
* direct transactional handlers, persist the latest aggregate state plus a diagnostic event journal,
|
|
76
|
+
* and hand already-stored events to the event bus. Aggregate and process-manager command
|
|
77
|
+
* execution require `command.id` before routing or mutation so produced events
|
|
78
|
+
* can carry a contract-valid command origin. With authentic projection
|
|
79
|
+
* subscriber metadata, built contexts can also execute projection subscribers
|
|
80
|
+
* and write changed projection state through the context-owned `Stand`. With
|
|
81
|
+
* authentic process-manager metadata, built contexts can execute command
|
|
82
|
+
* assignees, event reactors, and event-commanding handlers, storing changed
|
|
83
|
+
* process-manager state through tenant-scoped Stand records with numeric
|
|
84
|
+
* versions. The `events` option declares generated event schemas emitted by
|
|
85
|
+
* aggregate and process-manager producer handlers. The repository surface still
|
|
86
|
+
* does not expose direct entity lookup/storage APIs, inbox/delivery management,
|
|
87
|
+
* caches, lifecycle monitors, or transport startup.
|
|
88
|
+
*
|
|
89
|
+
* @typeParam EntityType - A single concrete aggregate, projection, or process-manager constructor
|
|
90
|
+
* with one concrete generated state schema. Broad constructor, constructor-union, broad-schema, and
|
|
91
|
+
* schema-union bindings intentionally fail the public type constraint.
|
|
92
|
+
*/
|
|
93
|
+
export class Repository {
|
|
94
|
+
#entityType;
|
|
95
|
+
#entityFamily;
|
|
96
|
+
#metadata;
|
|
97
|
+
#routing;
|
|
98
|
+
/**
|
|
99
|
+
* Creates repository identity for one entity family and state schema pair.
|
|
100
|
+
*
|
|
101
|
+
* @param options The entity constructor, state schema, and optional routing configuration.
|
|
102
|
+
*/
|
|
103
|
+
constructor(options) {
|
|
104
|
+
if (!RepositoryIdentity.isRepositoryOptionsObject(options)) {
|
|
105
|
+
throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "Repository options must be a non-null object with an entity type class constructor " +
|
|
106
|
+
"extending Aggregate, Projection, or ProcessManager.");
|
|
107
|
+
}
|
|
108
|
+
const entityType = RepositoryIdentity.readEntityTypeOption(options);
|
|
109
|
+
const entityTypeDisplayName = RepositoryIdentity.entityTypeName(entityType);
|
|
110
|
+
if (typeof entityType !== "function" || !RepositoryIdentity.isClassConstructor(entityType)) {
|
|
111
|
+
throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", `Repository entity type "${entityTypeDisplayName}" must be a class constructor ` +
|
|
112
|
+
"extending Aggregate, Projection, or ProcessManager.");
|
|
113
|
+
}
|
|
114
|
+
const entityFamily = RepositoryIdentity.resolveRepositoryEntityFamily(entityType);
|
|
115
|
+
if (entityFamily === undefined) {
|
|
116
|
+
throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", `Repository entity type "${entityTypeDisplayName}" must extend Aggregate, Projection, or ProcessManager.`);
|
|
117
|
+
}
|
|
118
|
+
const schema = RepositoryIdentity.readRepositorySchemaOption(options, entityTypeDisplayName, entityFamily);
|
|
119
|
+
const metadata = RepositoryIdentity.describeRepositoryEntityMetadata(entityTypeDisplayName, entityFamily, schema);
|
|
120
|
+
if (metadata.kind !== entityFamily) {
|
|
121
|
+
throw new RepositoryIdentityError("ENTITY_SCHEMA_KIND_MISMATCH", `Repository entity type "${entityTypeDisplayName}" does not match ` +
|
|
122
|
+
"the supplied state schema.");
|
|
123
|
+
}
|
|
124
|
+
if (options.stateUpdateRouting !== undefined && entityFamily !== "projection") {
|
|
125
|
+
throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "State-update routing is supported only by Projection repositories.");
|
|
126
|
+
}
|
|
127
|
+
attachEntitySchema(entityType, schema);
|
|
128
|
+
this.#entityType = entityType;
|
|
129
|
+
this.#entityFamily = entityFamily;
|
|
130
|
+
this.#metadata = metadata;
|
|
131
|
+
this.#routing = RepositoryRoutes.createRepositoryRouting(this.#entityType, this.#entityFamily, this.#metadata, options.handlers, options.events ?? [], CommandRoutingInternals.snapshot(options.commandRouting), EventRoutingInternals.snapshot(options.eventRouting), StateUpdateRoutingInternals.snapshot(options.stateUpdateRouting), new StringifierRegistry(options.stringifierRegistry));
|
|
132
|
+
repositoryRoutings.set(this, this.#routing);
|
|
133
|
+
repositoryProducedEventSchemas.set(this, Object.freeze([...this.#routing.producedEventSchemas]));
|
|
134
|
+
repositoryHistoryConfigurations.set(this, RepositoryStorage.readHistoryConfiguration(options, this.#entityFamily));
|
|
135
|
+
repositorySnapshots.set(this, RepositoryIdentity.createRepositorySnapshot(this.#entityType, this.#entityFamily, this.#metadata));
|
|
136
|
+
repositoryDispatchers.set(this, RepositoryDispatch.createRepositoryDispatchers(this, this.#routing));
|
|
137
|
+
const entityInboxTarget = RepositoryDispatch.createEntityInboxTarget(this, this.#routing);
|
|
138
|
+
const projectionInboxTarget = RepositoryDispatch.createProjectionInboxTarget(this, this.#routing);
|
|
139
|
+
if (entityInboxTarget !== undefined) {
|
|
140
|
+
repositoryEntityInboxTargets.set(this, entityInboxTarget);
|
|
141
|
+
}
|
|
142
|
+
if (projectionInboxTarget !== undefined) {
|
|
143
|
+
repositoryProjectionInboxTargets.set(this, projectionInboxTarget);
|
|
144
|
+
repositoryProjectionDirect.set(this, RepositoryDispatch.createProjectionDirectDispatch(this, this.#routing));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns the entity constructor owned by this repository identity.
|
|
149
|
+
*
|
|
150
|
+
* @returns The owned entity constructor.
|
|
151
|
+
*/
|
|
152
|
+
get entityType() {
|
|
153
|
+
return this.#entityType;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Returns the entity family inferred from its constructor.
|
|
157
|
+
*
|
|
158
|
+
* @returns The aggregate, projection, or process-manager family.
|
|
159
|
+
*/
|
|
160
|
+
get entityFamily() {
|
|
161
|
+
return this.#entityFamily;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Returns the generated schema for this repository's entity state.
|
|
165
|
+
*
|
|
166
|
+
* @returns The owned state schema.
|
|
167
|
+
*/
|
|
168
|
+
get stateSchema() {
|
|
169
|
+
return this.#metadata.schema;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Sets whether future state-history rows are appended for this repository.
|
|
173
|
+
*
|
|
174
|
+
* This JVM-parity switch is for controlled administration/testing, not
|
|
175
|
+
* routine request-time behavior. It never deletes or reconstructs history.
|
|
176
|
+
*
|
|
177
|
+
* @param enabled Whether later successful stores append state-history rows.
|
|
178
|
+
*/
|
|
179
|
+
setStateHistoryEnabled(enabled) {
|
|
180
|
+
if (typeof enabled !== "boolean") {
|
|
181
|
+
throw new TypeError("Repository state-history switch requires a boolean.");
|
|
182
|
+
}
|
|
183
|
+
const configuration = RepositoryStorage.historyConfiguration(this);
|
|
184
|
+
if (configuration.stateHistory === enabled)
|
|
185
|
+
return;
|
|
186
|
+
configuration.stateHistory = enabled;
|
|
187
|
+
const handles = repositoryEntityHandles.get(this);
|
|
188
|
+
if (handles !== undefined) {
|
|
189
|
+
for (const handle of handles.values())
|
|
190
|
+
handle.close();
|
|
191
|
+
handles.clear();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Returns descriptor-derived metadata for the owned entity state.
|
|
196
|
+
*
|
|
197
|
+
* @returns A metadata view for the owned state.
|
|
198
|
+
*/
|
|
199
|
+
get metadata() {
|
|
200
|
+
return this.#metadata;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Returns the fully qualified Protobuf name of the owned entity state.
|
|
204
|
+
*
|
|
205
|
+
* @returns The state message type name.
|
|
206
|
+
*/
|
|
207
|
+
get stateFullTypeName() {
|
|
208
|
+
return this.#metadata.fullTypeName;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Returns a copy of the canonical entity ID field metadata.
|
|
212
|
+
*
|
|
213
|
+
* @returns The descriptor field used as the entity ID.
|
|
214
|
+
*/
|
|
215
|
+
get idField() {
|
|
216
|
+
return RepositoryIdentity.cloneFieldMetadata(this.#metadata.idField);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Returns a copy-safe identity snapshot for builder duplicate and conflict checks.
|
|
220
|
+
*
|
|
221
|
+
* @returns An immutable snapshot of the repository identity.
|
|
222
|
+
*/
|
|
223
|
+
get snapshot() {
|
|
224
|
+
return RepositoryIdentity.cloneRepositorySnapshot(RepositoryIdentity.createRepositorySnapshot(this.#entityType, this.#entityFamily, this.#metadata));
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Routes a command to one entity ID without invoking a handler.
|
|
228
|
+
*
|
|
229
|
+
* @param command The command envelope to route.
|
|
230
|
+
* @returns The calculated command route.
|
|
231
|
+
*/
|
|
232
|
+
routeCommand(command) {
|
|
233
|
+
return this.#routing.routeCommand(command);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Routes an Event without invoking a handler.
|
|
237
|
+
*
|
|
238
|
+
* The default route uses a readable producer whose typed ID is compatible
|
|
239
|
+
* with the Entity ID. A valid producer of another type falls back to the
|
|
240
|
+
* Event's declaration-first field. A producer that claims the compatible type
|
|
241
|
+
* but cannot be decoded is rejected. Custom routes may select zero, one, or
|
|
242
|
+
* many targets.
|
|
243
|
+
*
|
|
244
|
+
* @param event The event envelope to route.
|
|
245
|
+
* @returns The calculated event route.
|
|
246
|
+
*/
|
|
247
|
+
routeEvent(event) {
|
|
248
|
+
return this.#routing.routeEvent(event);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const repositorySnapshots = new WeakMap();
|
|
252
|
+
const repositoryRoutings = new WeakMap();
|
|
253
|
+
const repositoryProducedEventSchemas = new WeakMap();
|
|
254
|
+
const repositoryDispatchers = new WeakMap();
|
|
255
|
+
const repositoryEntityInboxTargets = new WeakMap();
|
|
256
|
+
const repositoryProjectionInboxTargets = new WeakMap();
|
|
257
|
+
const repositoryProjectionDirect = new WeakMap();
|
|
258
|
+
const repositoryRuntimes = new WeakMap();
|
|
259
|
+
const repositoryEntityHandles = new WeakMap();
|
|
260
|
+
const entityStateHistoryCaches = new WeakMap();
|
|
261
|
+
const repositoryHistoryConfigurations = new WeakMap();
|
|
262
|
+
const repositoryDispatchGuards = new WeakMap();
|
|
263
|
+
Object.freeze(Repository);
|
|
264
|
+
/**
|
|
265
|
+
* Exposes framework-only repository access for bounded-context assembly.
|
|
266
|
+
*
|
|
267
|
+
* @internal
|
|
268
|
+
*/
|
|
269
|
+
export const repositoryAccess = Object.freeze({
|
|
270
|
+
hasInstance(repository) {
|
|
271
|
+
return repositorySnapshots.has(repository);
|
|
272
|
+
},
|
|
273
|
+
snapshot(repository) {
|
|
274
|
+
const snapshot = repositorySnapshots.get(repository);
|
|
275
|
+
if (snapshot === undefined) {
|
|
276
|
+
throw new TypeError("Repository snapshot requires a Repository instance.");
|
|
277
|
+
}
|
|
278
|
+
return RepositoryIdentity.cloneRepositorySnapshot(snapshot);
|
|
279
|
+
},
|
|
280
|
+
producedEventSchemas(repository) {
|
|
281
|
+
const schemas = repositoryProducedEventSchemas.get(repository);
|
|
282
|
+
if (schemas === undefined) {
|
|
283
|
+
throw new TypeError("Produced event schemas require a Repository instance.");
|
|
284
|
+
}
|
|
285
|
+
return schemas;
|
|
286
|
+
},
|
|
287
|
+
commandDispatcher(repository) {
|
|
288
|
+
return repositoryDispatchers.get(repository)?.command;
|
|
289
|
+
},
|
|
290
|
+
eventDispatcher(repository) {
|
|
291
|
+
return repositoryDispatchers.get(repository)?.event;
|
|
292
|
+
},
|
|
293
|
+
systemEventDispatcher(repository) {
|
|
294
|
+
return repositoryDispatchers.get(repository)?.systemEvent;
|
|
295
|
+
},
|
|
296
|
+
stateSubscriptionTypes(repository) {
|
|
297
|
+
const routing = repositoryRoutings.get(repository);
|
|
298
|
+
if (routing === undefined) {
|
|
299
|
+
throw new TypeError("State subscriptions require a Repository instance.");
|
|
300
|
+
}
|
|
301
|
+
return Object.freeze(routing.stateSchemas.map((schema) => schema.typeName));
|
|
302
|
+
},
|
|
303
|
+
routeStateUpdate(repository, event) {
|
|
304
|
+
const routing = repositoryRoutings.get(repository);
|
|
305
|
+
if (routing === undefined) {
|
|
306
|
+
throw new TypeError("State-update routing requires a Repository instance.");
|
|
307
|
+
}
|
|
308
|
+
return routing.routeStateUpdate(event);
|
|
309
|
+
},
|
|
310
|
+
entityInboxTarget(repository) {
|
|
311
|
+
return repositoryEntityInboxTargets.get(repository);
|
|
312
|
+
},
|
|
313
|
+
projectionInboxTarget(repository) {
|
|
314
|
+
return repositoryProjectionInboxTargets.get(repository);
|
|
315
|
+
},
|
|
316
|
+
dispatchProjectionDirect(repository, event, rebuild) {
|
|
317
|
+
const dispatch = repositoryProjectionDirect.get(repository);
|
|
318
|
+
if (dispatch === undefined) {
|
|
319
|
+
throw new TypeError("Direct projection dispatch requires a projection Repository instance.");
|
|
320
|
+
}
|
|
321
|
+
return dispatch(event, rebuild);
|
|
322
|
+
},
|
|
323
|
+
bindRuntime(repository, runtime) {
|
|
324
|
+
repositoryRuntimes.set(repository, Object.freeze(runtime));
|
|
325
|
+
},
|
|
326
|
+
clearRuntime(repository) {
|
|
327
|
+
repositoryRuntimes.delete(repository);
|
|
328
|
+
repositoryDispatchGuards.delete(repository);
|
|
329
|
+
const handles = repositoryEntityHandles.get(repository);
|
|
330
|
+
if (handles !== undefined) {
|
|
331
|
+
for (const handle of handles.values())
|
|
332
|
+
handle.close();
|
|
333
|
+
handles.clear();
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
const inboxDedupMs = 30_000;
|
|
338
|
+
class AggregateExecutionSupport {
|
|
339
|
+
#repository;
|
|
340
|
+
#runtime;
|
|
341
|
+
#storageContext;
|
|
342
|
+
constructor(repository, runtime, storageContext) {
|
|
343
|
+
this.#repository = repository;
|
|
344
|
+
this.#runtime = runtime;
|
|
345
|
+
this.#storageContext = storageContext;
|
|
346
|
+
}
|
|
347
|
+
async loadAggregate(entityId) {
|
|
348
|
+
const storageInput = RepositoryStorage.entityStorageInput(this.#repository, this.#storageContext);
|
|
349
|
+
const storage = RepositoryStorage.openRepositoryEntityStorage(this.#repository, this.#runtime.storageFactory, storageInput);
|
|
350
|
+
const current = await standAccess.readCurrent(this.#runtime.stand, this.#repository.stateSchema, entityId, this.#storageContext.tenantId === undefined
|
|
351
|
+
? {}
|
|
352
|
+
: { tenantId: this.#storageContext.tenantId });
|
|
353
|
+
const entity = this.#instantiateAggregate(entityId, current);
|
|
354
|
+
RepositoryHistoryInternals.bindEntityHistory(entity, storage, entityId, this.#repository.stateSchema);
|
|
355
|
+
return Object.freeze({
|
|
356
|
+
commits: storage.commits,
|
|
357
|
+
current: current === undefined
|
|
358
|
+
? undefined
|
|
359
|
+
: EntityRecords.pack(this.#repository.stateSchema, entityId, current.state, current.versionMessage, { archived: current.archived, deleted: current.deleted }),
|
|
360
|
+
entity,
|
|
361
|
+
oldState: current === undefined
|
|
362
|
+
? undefined
|
|
363
|
+
: clone(this.#repository.stateSchema, current.state),
|
|
364
|
+
states: storage.states,
|
|
365
|
+
events: storage.events,
|
|
366
|
+
version: current?.version ?? 0n,
|
|
367
|
+
storageInput,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
normalizeProducedSignals(produced) {
|
|
371
|
+
if (produced === undefined) {
|
|
372
|
+
return Object.freeze([]);
|
|
373
|
+
}
|
|
374
|
+
if (Array.isArray(produced)) {
|
|
375
|
+
return Object.freeze(Array.from(produced));
|
|
376
|
+
}
|
|
377
|
+
return Object.freeze([produced]);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Store the current aggregate record before its diagnostic and delivery journals.
|
|
381
|
+
*/
|
|
382
|
+
async persistAggregateUpdate(loaded, entityId, version, events) {
|
|
383
|
+
const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
|
|
384
|
+
const state = RepositoryEntities.repositoryState(loaded.entity);
|
|
385
|
+
const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(this.#storageContext.tenantId, create(VersionSchema, { number: RepositorySignals.eventVersionNumber(version) }), lifecycle));
|
|
386
|
+
try {
|
|
387
|
+
const createdAt = events[events.length - 1]?.context?.timestamp ?? create(TimestampSchema);
|
|
388
|
+
const outcome = await loaded.commits.commit({
|
|
389
|
+
context: this.#storageContext,
|
|
390
|
+
entity: loaded.storageInput,
|
|
391
|
+
entityId,
|
|
392
|
+
...(loaded.current === undefined ? {} : { expected: loaded.current }),
|
|
393
|
+
next: EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle),
|
|
394
|
+
...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
|
|
395
|
+
? {
|
|
396
|
+
states: [
|
|
397
|
+
EntityRecords.pack(this.#repository.stateSchema, entityId, state, create(VersionSchema, {
|
|
398
|
+
number: RepositorySignals.eventVersionNumber(version),
|
|
399
|
+
timestamp: createdAt,
|
|
400
|
+
}), lifecycle),
|
|
401
|
+
],
|
|
402
|
+
}
|
|
403
|
+
: {}),
|
|
404
|
+
diagnostics: events.map((event) => clone(EventSchema, event)),
|
|
405
|
+
events,
|
|
406
|
+
});
|
|
407
|
+
if (outcome !== "committed") {
|
|
408
|
+
deferred.cancel();
|
|
409
|
+
throw new Error("Concurrent Aggregate state commit conflict.");
|
|
410
|
+
}
|
|
411
|
+
entityStateHistoryCaches.get(loaded.entity)?.clear();
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
deferred.cancel();
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
deferred.notify();
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
const event = events[events.length - 1];
|
|
422
|
+
if (event !== undefined)
|
|
423
|
+
this.#runtime.recordDispatchFailure(event, error);
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
async appendDiagnosticEvent(loaded, entityId, event) {
|
|
428
|
+
await loaded.events.append(clone(EventSchema, event));
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Complete persistence before scheduling best-effort stored-event dispatch.
|
|
432
|
+
*/
|
|
433
|
+
async persistAggregateAndDispatch(loaded, entityId, version, events, dispatch, onPersisted) {
|
|
434
|
+
const committed = await this.persistAggregateUpdate(loaded, entityId, version, events);
|
|
435
|
+
if (!committed)
|
|
436
|
+
return () => Promise.resolve();
|
|
437
|
+
await onPersisted();
|
|
438
|
+
return async () => {
|
|
439
|
+
await Promise.all(events.map(async (event) => {
|
|
440
|
+
try {
|
|
441
|
+
await dispatch(event);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
this.#runtime.recordDispatchFailure(event, error);
|
|
445
|
+
}
|
|
446
|
+
}));
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
#instantiateAggregate(entityId, current) {
|
|
450
|
+
const entityType = this.#repository.entityType;
|
|
451
|
+
const options = {
|
|
452
|
+
id: entityId,
|
|
453
|
+
schema: this.#repository.stateSchema,
|
|
454
|
+
state: current?.state ?? this.#defaultState(entityId),
|
|
455
|
+
version: current?.version ?? 0n,
|
|
456
|
+
};
|
|
457
|
+
if (current !== undefined) {
|
|
458
|
+
options.lifecycle = { archived: current.archived, deleted: current.deleted };
|
|
459
|
+
}
|
|
460
|
+
return new entityType(options);
|
|
461
|
+
}
|
|
462
|
+
#defaultState(entityId) {
|
|
463
|
+
return create(this.#repository.stateSchema, {
|
|
464
|
+
[this.#repository.idField.localName]: entityId,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
class AggregateCommandExecution {
|
|
469
|
+
#repository;
|
|
470
|
+
#routing;
|
|
471
|
+
#runtime;
|
|
472
|
+
#command;
|
|
473
|
+
#support;
|
|
474
|
+
constructor(repository, routing, runtime, command) {
|
|
475
|
+
this.#repository = repository;
|
|
476
|
+
this.#routing = routing;
|
|
477
|
+
this.#runtime = runtime;
|
|
478
|
+
this.#command = command;
|
|
479
|
+
this.#support = new AggregateExecutionSupport(repository, runtime, RepositoryTenants.storageContextForCommand(this.#runtime.context, this.#command));
|
|
480
|
+
}
|
|
481
|
+
async run(replayedRoute) {
|
|
482
|
+
void RepositorySignals.requireCommandId(this.#command);
|
|
483
|
+
const commandMessage = EntityInvocation.requireSignalMessage(this.#command.message, "command");
|
|
484
|
+
const commandSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.commandSchemas, commandMessage.typeUrl, "command");
|
|
485
|
+
const message = EntityInvocation.unpackRequired(commandMessage, commandSchema, "command");
|
|
486
|
+
const route = replayedRoute ?? this.#repository.routeCommand(this.#command);
|
|
487
|
+
const assignee = this.#routing.commandReadiness?.findCommandAssignee(route.messageFullTypeName);
|
|
488
|
+
if (assignee === undefined) {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
const loaded = await this.#support.loadAggregate(route.entityId);
|
|
492
|
+
HandlerDispatchPublisher.command(this.#runtime, this.#repository, this.#command, route.entityId);
|
|
493
|
+
const commandContext = EntityInvocation.commandHandlerContext(this.#command);
|
|
494
|
+
let produced;
|
|
495
|
+
try {
|
|
496
|
+
produced = await this.#invokeAssignee(loaded.entity, assignee.handler.methodName, message, assignee.handler.parameterCount, commandContext);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
if (!RejectionThrowable.is(error)) {
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
return RepositorySignals.postRejectionEvent(this.#runtime, this.#repository, this.#command, route.entityId, error);
|
|
503
|
+
}
|
|
504
|
+
const events = this.#bindProducedEvents(this.#support.normalizeProducedSignals(produced), route.entityId, loaded.version, true);
|
|
505
|
+
if (events.length === 0) {
|
|
506
|
+
throw new Error("Repository aggregate command handlers must return at least one event.");
|
|
507
|
+
}
|
|
508
|
+
const committedVersion = loaded.version + BigInt(events.length);
|
|
509
|
+
return await this.#support.persistAggregateAndDispatch(loaded, route.entityId, committedVersion, events, (event) => this.#runtime.dispatchStored(event), () => {
|
|
510
|
+
if (!RepositoryEntities.repositoryChanged(loaded.entity))
|
|
511
|
+
return;
|
|
512
|
+
EntityStateChangePublisher.command(this.#runtime, this.#repository, this.#command, route.entityId, loaded.oldState, loaded.current === undefined
|
|
513
|
+
? undefined
|
|
514
|
+
: {
|
|
515
|
+
archived: loaded.current.lifecycleFlags?.archived ?? false,
|
|
516
|
+
deleted: loaded.current.lifecycleFlags?.deleted ?? false,
|
|
517
|
+
}, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositorySignals.eventVersionNumber(committedVersion));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async #invokeAssignee(entity, methodName, message, parameterCount, context) {
|
|
521
|
+
transactionalEntityAccess.start(entity);
|
|
522
|
+
try {
|
|
523
|
+
const produced = await EntityInvocation.invokeEntityMethod(entity, methodName, message, parameterCount, context);
|
|
524
|
+
const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
|
|
525
|
+
if (commit.status === "rejected") {
|
|
526
|
+
throw new TransitionValidationError(commit.validation.error);
|
|
527
|
+
}
|
|
528
|
+
return produced;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
transactionalEntityAccess.rollback(entity);
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
#bindProducedEvents(produced, entityId, lastVersion, allowEnvelopes) {
|
|
536
|
+
const dispatchVersion = lastVersion + 1n;
|
|
537
|
+
let sequence = 0;
|
|
538
|
+
return Object.freeze(produced.map((signal) => {
|
|
539
|
+
sequence += 1;
|
|
540
|
+
const version = allowEnvelopes ? lastVersion + BigInt(sequence) : dispatchVersion;
|
|
541
|
+
return this.#bindProducedEvent(signal, entityId, version, allowEnvelopes, sequence);
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
#bindProducedEvent(signal, entityId, version, allowEnvelopes, sequence) {
|
|
545
|
+
const metadata = this.#runtime.signalMetadata.eventFromCommand(this.#command, sequence, {
|
|
546
|
+
version: RepositorySignals.eventVersionNumber(version),
|
|
547
|
+
});
|
|
548
|
+
const bound = allowEnvelopes && EntityInvocation.isEventEnvelope(signal)
|
|
549
|
+
? clone(EventSchema, signal)
|
|
550
|
+
: this.#packDomainEvent(signal, metadata);
|
|
551
|
+
bound.context = RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId);
|
|
552
|
+
return bound;
|
|
553
|
+
}
|
|
554
|
+
#packDomainEvent(message, metadata) {
|
|
555
|
+
const typeName = EntityInvocation.messageTypeName(message);
|
|
556
|
+
const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
|
|
557
|
+
if (schema === undefined) {
|
|
558
|
+
throw new Error(`Repository aggregate execution cannot pack event message "${typeName}".`);
|
|
559
|
+
}
|
|
560
|
+
return create(EventSchema, {
|
|
561
|
+
id: metadata.id,
|
|
562
|
+
message: AnyMessages.pack(schema, message),
|
|
563
|
+
context: metadata.context,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
class AggregateEventExecution {
|
|
568
|
+
#repository;
|
|
569
|
+
#routing;
|
|
570
|
+
#runtime;
|
|
571
|
+
#event;
|
|
572
|
+
#support;
|
|
573
|
+
constructor(repository, routing, runtime, event) {
|
|
574
|
+
this.#repository = repository;
|
|
575
|
+
this.#routing = routing;
|
|
576
|
+
this.#runtime = runtime;
|
|
577
|
+
this.#event = event;
|
|
578
|
+
this.#support = new AggregateExecutionSupport(repository, runtime, RepositoryTenants.storageContextForEvent(this.#runtime.context, this.#event));
|
|
579
|
+
}
|
|
580
|
+
async runTarget(entityId, acceptedRoute) {
|
|
581
|
+
const intake = this.#readIntake(acceptedRoute);
|
|
582
|
+
if (intake.reactors.length === 0 && intake.commanders.length === 0) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
await this.#postCommands(await this.#executeEntity(entityId, intake));
|
|
586
|
+
}
|
|
587
|
+
#readIntake(acceptedRoute) {
|
|
588
|
+
const eventMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
|
|
589
|
+
const eventSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.eventSchemas, eventMessage.typeUrl, "event");
|
|
590
|
+
const message = EntityInvocation.unpackRequired(eventMessage, eventSchema, "event");
|
|
591
|
+
const route = acceptedRoute;
|
|
592
|
+
const reactors = this.#routing
|
|
593
|
+
.eventReactors(route.messageFullTypeName, message, this.#event.context?.external === true)
|
|
594
|
+
.filter((reactor) => RepositoryHandlers.handlerEmittedSchemas(reactor.handler).length > 0);
|
|
595
|
+
const commanders = this.#routing.commandReactions(route.messageFullTypeName, message, this.#event.context?.external === true);
|
|
596
|
+
return Object.freeze({
|
|
597
|
+
message,
|
|
598
|
+
route,
|
|
599
|
+
reactors: Object.freeze([...reactors]),
|
|
600
|
+
commanders,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
async #executeEntity(entityId, intake) {
|
|
604
|
+
const loaded = await this.#support.loadAggregate(entityId);
|
|
605
|
+
const commands = [];
|
|
606
|
+
const produced = await this.#invokeHandlers(entityId, loaded, intake, intake.route.entityIds.length > 1);
|
|
607
|
+
for (const command of produced.commands) {
|
|
608
|
+
commands.push(command);
|
|
609
|
+
}
|
|
610
|
+
if (produced.events.length > 0) {
|
|
611
|
+
const dispatch = await this.#support.persistAggregateAndDispatch(loaded, entityId, loaded.version + BigInt(produced.events.length), produced.events, (event) => this.#runtime.dispatchStoredFollowUp(event), () => {
|
|
612
|
+
if (!RepositoryEntities.repositoryChanged(loaded.entity))
|
|
613
|
+
return;
|
|
614
|
+
EntityStateChangePublisher.event(this.#runtime, this.#repository, this.#event, entityId, loaded.oldState, loaded.current === undefined
|
|
615
|
+
? undefined
|
|
616
|
+
: {
|
|
617
|
+
archived: loaded.current.lifecycleFlags?.archived ?? false,
|
|
618
|
+
deleted: loaded.current.lifecycleFlags?.deleted ?? false,
|
|
619
|
+
}, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositorySignals.eventVersionNumber(loaded.version + BigInt(produced.events.length)));
|
|
620
|
+
});
|
|
621
|
+
void dispatch();
|
|
622
|
+
}
|
|
623
|
+
await this.#support.appendDiagnosticEvent(loaded, entityId, DispatchGuards.guardedJournalEvent(this.#repository, this.#event, entityId));
|
|
624
|
+
return Object.freeze(commands);
|
|
625
|
+
}
|
|
626
|
+
async #invokeHandlers(entityId, loaded, intake, multiTarget) {
|
|
627
|
+
const eventContext = EntityInvocation.eventHandlerContext(this.#event);
|
|
628
|
+
const commands = [];
|
|
629
|
+
const events = [];
|
|
630
|
+
transactionalEntityAccess.start(loaded.entity);
|
|
631
|
+
try {
|
|
632
|
+
for (const commander of intake.commanders) {
|
|
633
|
+
const produced = await EntityInvocation.invokeEntityMethod(loaded.entity, commander.handler.methodName, intake.message, commander.handler.parameterCount, eventContext);
|
|
634
|
+
commands.push(...this.#bindProducedCommands(this.#support.normalizeProducedSignals(produced)));
|
|
635
|
+
}
|
|
636
|
+
if (intake.reactors.length > 0) {
|
|
637
|
+
HandlerDispatchPublisher.reactor(this.#runtime, this.#repository, this.#event, entityId);
|
|
638
|
+
}
|
|
639
|
+
for (const reactor of intake.reactors) {
|
|
640
|
+
const produced = await EntityInvocation.invokeEntityMethod(loaded.entity, reactor.handler.methodName, intake.message, reactor.handler.parameterCount, eventContext);
|
|
641
|
+
events.push(...this.#bindProducedEvents(this.#support.normalizeProducedSignals(produced), entityId, loaded.version, multiTarget));
|
|
642
|
+
}
|
|
643
|
+
const commit = await commitFenced(loaded.entity, (current) => transactionalEntityAccess.commit(current));
|
|
644
|
+
if (commit.status === "rejected") {
|
|
645
|
+
throw new TransitionValidationError(commit.validation.error);
|
|
646
|
+
}
|
|
647
|
+
return Object.freeze({
|
|
648
|
+
commands: Object.freeze(commands),
|
|
649
|
+
events: Object.freeze(events),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
transactionalEntityAccess.rollback(loaded.entity);
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async #postCommands(commands) {
|
|
658
|
+
for (const command of commands) {
|
|
659
|
+
await this.#runtime.onPostCommand(command);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
#bindProducedEvents(produced, entityId, lastVersion, multiTarget) {
|
|
663
|
+
const version = lastVersion + 1n;
|
|
664
|
+
let sequence = 0;
|
|
665
|
+
return Object.freeze(produced.map((signal) => {
|
|
666
|
+
sequence += 1;
|
|
667
|
+
return this.#bindProducedEvent(signal, entityId, version, sequence, multiTarget);
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
#bindProducedEvent(signal, entityId, version, sequence, multiTarget) {
|
|
671
|
+
const typeName = EntityInvocation.messageTypeName(signal);
|
|
672
|
+
const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
|
|
673
|
+
if (schema === undefined) {
|
|
674
|
+
throw new Error(`Repository aggregate execution cannot pack event message "${typeName}".`);
|
|
675
|
+
}
|
|
676
|
+
const metadata = this.#runtime.signalMetadata.eventFromEvent(this.#event, sequence, {
|
|
677
|
+
version: RepositorySignals.eventVersionNumber(version),
|
|
678
|
+
});
|
|
679
|
+
return create(EventSchema, {
|
|
680
|
+
id: multiTarget
|
|
681
|
+
? create(EventIdSchema, {
|
|
682
|
+
value: `${metadata.id.value}.target.` +
|
|
683
|
+
encodeURIComponent(DispatchGuards.canonicalEntityIdKey(this.#repository, entityId)),
|
|
684
|
+
})
|
|
685
|
+
: metadata.id,
|
|
686
|
+
message: AnyMessages.pack(schema, signal),
|
|
687
|
+
context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId),
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
#bindProducedCommands(produced) {
|
|
691
|
+
let sequence = 0;
|
|
692
|
+
return Object.freeze(produced.map((signal) => {
|
|
693
|
+
sequence += 1;
|
|
694
|
+
const typeName = EntityInvocation.messageTypeName(signal);
|
|
695
|
+
const schema = this.#routing.producedCommandSchemas.find((candidate) => candidate.typeName === typeName);
|
|
696
|
+
if (schema === undefined) {
|
|
697
|
+
throw new Error(`Repository aggregate execution cannot pack command message "${typeName}".`);
|
|
698
|
+
}
|
|
699
|
+
const metadata = this.#runtime.signalMetadata.commandFromEvent(this.#event, sequence);
|
|
700
|
+
return create(CommandSchema, {
|
|
701
|
+
id: metadata.id,
|
|
702
|
+
message: AnyMessages.pack(schema, signal),
|
|
703
|
+
context: metadata.context,
|
|
704
|
+
});
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
class ProjectionEventExecution {
|
|
709
|
+
#repository;
|
|
710
|
+
#routing;
|
|
711
|
+
#runtime;
|
|
712
|
+
#event;
|
|
713
|
+
#rebuild;
|
|
714
|
+
constructor(repository, routing, runtime, event, rebuild = false) {
|
|
715
|
+
this.#repository = repository;
|
|
716
|
+
this.#routing = routing;
|
|
717
|
+
this.#runtime = runtime;
|
|
718
|
+
this.#event = event;
|
|
719
|
+
this.#rebuild = rebuild;
|
|
720
|
+
}
|
|
721
|
+
static async runStateTarget(repository, routing, runtime, event, entityId, route, subscribers) {
|
|
722
|
+
const execution = new ProjectionEventExecution(repository, routing, runtime, event);
|
|
723
|
+
await execution.#executeStateTarget(entityId, route.state, subscribers);
|
|
724
|
+
}
|
|
725
|
+
async run(acceptedRoute) {
|
|
726
|
+
const intake = this.#readIntake(acceptedRoute);
|
|
727
|
+
if (intake.subscribers.length === 0) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
for (const entityId of intake.route.entityIds) {
|
|
731
|
+
await InboxHandoff.handoffProjectionEvent(this.#repository, this.#runtime, this.#event, entityId);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async runTarget(entityId, acceptedRoute) {
|
|
735
|
+
const intake = this.#readIntake(acceptedRoute);
|
|
736
|
+
if (intake.subscribers.length === 0) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
await this.#executeTarget(entityId, intake.subscribers);
|
|
740
|
+
}
|
|
741
|
+
async runDirect(acceptedRoute) {
|
|
742
|
+
const intake = this.#readIntake(acceptedRoute);
|
|
743
|
+
if (intake.subscribers.length === 0) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
for (const entityId of intake.route.entityIds) {
|
|
747
|
+
await this.#executeTarget(entityId, intake.subscribers);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async #executeTarget(entityId, subscribers) {
|
|
751
|
+
const packedMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
|
|
752
|
+
const tenantOptions = RepositoryTenants.standTenantOptions(this.#runtime.context, this.#event);
|
|
753
|
+
const mode = this.#rebuild ? "rebuild" : "stored";
|
|
754
|
+
const loaded = await this.#loadProjection(entityId, tenantOptions, mode);
|
|
755
|
+
HandlerDispatchPublisher.subscriber(this.#runtime, this.#repository, this.#event, entityId);
|
|
756
|
+
await this.#invokeSubscribers(loaded.entity, subscribers, packedMessage);
|
|
757
|
+
await this.#storeIfChanged(loaded, tenantOptions, loaded.current === undefined
|
|
758
|
+
? undefined
|
|
759
|
+
: EntityRecords.unpack(this.#repository.stateSchema, loaded.current).state, mode);
|
|
760
|
+
}
|
|
761
|
+
async #executeStateTarget(entityId, state, subscribers) {
|
|
762
|
+
const tenantOptions = RepositoryTenants.standTenantOptions(this.#runtime.context, this.#event);
|
|
763
|
+
const loaded = await this.#loadProjection(entityId, tenantOptions, "stored");
|
|
764
|
+
await this.#invokeStateSubscribers(loaded.entity, subscribers, state);
|
|
765
|
+
await this.#storeIfChanged(loaded, tenantOptions, loaded.current === undefined
|
|
766
|
+
? undefined
|
|
767
|
+
: EntityRecords.unpack(this.#repository.stateSchema, loaded.current).state, "stored");
|
|
768
|
+
}
|
|
769
|
+
#readIntake(acceptedRoute) {
|
|
770
|
+
const route = acceptedRoute ?? this.#repository.routeEvent(this.#event);
|
|
771
|
+
const packedMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
|
|
772
|
+
const eventSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.eventSchemas, packedMessage.typeUrl, "event");
|
|
773
|
+
const message = EntityInvocation.unpackRequired(packedMessage, eventSchema, "event");
|
|
774
|
+
const subscribers = this.#routing.eventSubscribers(route.messageFullTypeName, message, this.#event.context?.external === true);
|
|
775
|
+
return Object.freeze({
|
|
776
|
+
route,
|
|
777
|
+
subscribers,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async #storeIfChanged(loaded, tenantOptions, oldState, mode) {
|
|
781
|
+
if (!RepositoryEntities.repositoryChanged(loaded.entity)) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const entityId = loaded.entity.id;
|
|
785
|
+
const state = RepositoryEntities.repositoryState(loaded.entity);
|
|
786
|
+
const version = BigInt(this.#event.context?.version?.number ?? 0);
|
|
787
|
+
const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
|
|
788
|
+
const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(tenantOptions.tenantId, this.#event.context?.version, lifecycle));
|
|
789
|
+
try {
|
|
790
|
+
const outcome = await loaded.commits.commit({
|
|
791
|
+
context: loaded.storageInput.context,
|
|
792
|
+
entity: loaded.storageInput,
|
|
793
|
+
entityId,
|
|
794
|
+
...(loaded.current === undefined ? {} : { expected: loaded.current }),
|
|
795
|
+
next: EntityRecords.pack(this.#repository.stateSchema, entityId, state, this.#event.context?.version ?? version, lifecycle),
|
|
796
|
+
...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
|
|
797
|
+
? {
|
|
798
|
+
states: [
|
|
799
|
+
EntityRecords.pack(this.#repository.stateSchema, entityId, state, this.#event.context?.version ?? version, lifecycle),
|
|
800
|
+
],
|
|
801
|
+
}
|
|
802
|
+
: {}),
|
|
803
|
+
});
|
|
804
|
+
if (outcome !== "committed") {
|
|
805
|
+
deferred.cancel();
|
|
806
|
+
throw new Error("Concurrent Projection state commit conflict.");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
deferred.cancel();
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
deferred.notify();
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
this.#runtime.recordDispatchFailure(this.#event, error);
|
|
818
|
+
}
|
|
819
|
+
EntityStateChangePublisher.event(this.#runtime, this.#repository, this.#event, entityId, oldState, mode === "rebuild"
|
|
820
|
+
? lifecycle
|
|
821
|
+
: loaded.current === undefined
|
|
822
|
+
? undefined
|
|
823
|
+
: {
|
|
824
|
+
archived: loaded.current.lifecycleFlags?.archived ?? false,
|
|
825
|
+
deleted: loaded.current.lifecycleFlags?.deleted ?? false,
|
|
826
|
+
}, state, lifecycle, this.#event.context?.version?.number ?? 0);
|
|
827
|
+
}
|
|
828
|
+
async #invokeSubscribers(entity, subscribers, packedMessage) {
|
|
829
|
+
transactionalEntityAccess.start(entity);
|
|
830
|
+
try {
|
|
831
|
+
for (const subscriber of subscribers) {
|
|
832
|
+
const subscriberMessage = EntityInvocation.unpackRequired(packedMessage, subscriber.handler.schema, "event");
|
|
833
|
+
const eventContext = EntityInvocation.eventHandlerContext(this.#event);
|
|
834
|
+
await EntityInvocation.invokeEntityMethod(entity, subscriber.handler.methodName, subscriberMessage, subscriber.handler.parameterCount, eventContext);
|
|
835
|
+
}
|
|
836
|
+
const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
|
|
837
|
+
if (commit.status === "rejected") {
|
|
838
|
+
throw new TransitionValidationError(commit.validation.error);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
transactionalEntityAccess.rollback(entity);
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async #invokeStateSubscribers(entity, subscribers, state) {
|
|
847
|
+
transactionalEntityAccess.start(entity);
|
|
848
|
+
try {
|
|
849
|
+
for (const subscriber of subscribers) {
|
|
850
|
+
const context = EntityInvocation.eventHandlerContext(this.#event);
|
|
851
|
+
await EntityInvocation.invokeEntityMethod(entity, subscriber.handler.methodName, state, subscriber.handler.parameterCount, context);
|
|
852
|
+
}
|
|
853
|
+
const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
|
|
854
|
+
if (commit.status === "rejected")
|
|
855
|
+
throw new TransitionValidationError(commit.validation.error);
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
transactionalEntityAccess.rollback(entity);
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async #loadProjection(entityId, options, mode) {
|
|
863
|
+
const stored = await standAccess.readCurrent(this.#runtime.stand, this.#repository.stateSchema, entityId, options);
|
|
864
|
+
const entityType = this.#repository.entityType;
|
|
865
|
+
const entity = new entityType({
|
|
866
|
+
id: entityId,
|
|
867
|
+
schema: this.#repository.stateSchema,
|
|
868
|
+
state: stored === undefined || (mode === "rebuild" && stored.deleted)
|
|
869
|
+
? this.#defaultState(entityId)
|
|
870
|
+
: stored.state,
|
|
871
|
+
version: mode === "rebuild" && stored?.deleted
|
|
872
|
+
? 0
|
|
873
|
+
: RepositoryStand.projectionVersion(stored?.version),
|
|
874
|
+
lifecycle: mode === "rebuild" && stored?.deleted
|
|
875
|
+
? { archived: false, deleted: false }
|
|
876
|
+
: { archived: stored?.archived ?? false, deleted: stored?.deleted ?? false },
|
|
877
|
+
});
|
|
878
|
+
const storageInput = RepositoryStorage.entityStorageInput(this.#repository, RepositoryTenants.storageContextForTenant(this.#runtime.context, options.tenantId));
|
|
879
|
+
const storage = RepositoryStorage.openRepositoryEntityStorage(this.#repository, this.#runtime.storageFactory, storageInput);
|
|
880
|
+
RepositoryHistoryInternals.bindEntityHistory(entity, storage, entityId, this.#repository.stateSchema);
|
|
881
|
+
return Object.freeze({
|
|
882
|
+
commits: storage.commits,
|
|
883
|
+
current: stored === undefined
|
|
884
|
+
? undefined
|
|
885
|
+
: EntityRecords.pack(this.#repository.stateSchema, entityId, stored.state, stored.versionMessage, { archived: stored.archived, deleted: stored.deleted }),
|
|
886
|
+
entity,
|
|
887
|
+
storageInput,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
#defaultState(entityId) {
|
|
891
|
+
return create(this.#repository.stateSchema, {
|
|
892
|
+
[this.#repository.idField.localName]: entityId,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
class ProcessManagerExecutionSupport {
|
|
897
|
+
#repository;
|
|
898
|
+
#runtime;
|
|
899
|
+
constructor(repository, runtime) {
|
|
900
|
+
this.#repository = repository;
|
|
901
|
+
this.#runtime = runtime;
|
|
902
|
+
}
|
|
903
|
+
normalizeProducedSignals(produced) {
|
|
904
|
+
if (produced === undefined) {
|
|
905
|
+
return Object.freeze([]);
|
|
906
|
+
}
|
|
907
|
+
if (Array.isArray(produced)) {
|
|
908
|
+
return Object.freeze(Array.from(produced));
|
|
909
|
+
}
|
|
910
|
+
return Object.freeze([produced]);
|
|
911
|
+
}
|
|
912
|
+
async load(entityId, options) {
|
|
913
|
+
const stored = await standAccess.readCurrent(this.#runtime.stand, this.#repository.stateSchema, entityId, options);
|
|
914
|
+
const entityType = this.#repository.entityType;
|
|
915
|
+
const entity = new entityType({
|
|
916
|
+
id: entityId,
|
|
917
|
+
schema: this.#repository.stateSchema,
|
|
918
|
+
state: stored === undefined ? this.#defaultState(entityId) : stored.state,
|
|
919
|
+
version: RepositoryStand.projectionVersion(stored?.version),
|
|
920
|
+
lifecycle: { archived: stored?.archived ?? false, deleted: stored?.deleted ?? false },
|
|
921
|
+
});
|
|
922
|
+
const storageInput = RepositoryStorage.entityStorageInput(this.#repository, RepositoryTenants.storageContextForTenant(this.#runtime.context, options.tenantId));
|
|
923
|
+
const storage = RepositoryStorage.openRepositoryEntityStorage(this.#repository, this.#runtime.storageFactory, storageInput);
|
|
924
|
+
RepositoryHistoryInternals.bindEntityHistory(entity, storage, entityId, this.#repository.stateSchema);
|
|
925
|
+
return Object.freeze({
|
|
926
|
+
commits: storage.commits,
|
|
927
|
+
current: stored === undefined
|
|
928
|
+
? undefined
|
|
929
|
+
: EntityRecords.pack(this.#repository.stateSchema, entityId, stored.state, stored.versionMessage, { archived: stored.archived, deleted: stored.deleted }),
|
|
930
|
+
entity,
|
|
931
|
+
storageInput,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
async commit(loaded, options, createdAt, events) {
|
|
935
|
+
const changed = RepositoryEntities.repositoryChanged(loaded.entity);
|
|
936
|
+
if (!changed && events.length === 0)
|
|
937
|
+
return true;
|
|
938
|
+
const entityId = loaded.entity.id;
|
|
939
|
+
const state = RepositoryEntities.repositoryState(loaded.entity);
|
|
940
|
+
const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
|
|
941
|
+
const version = changed
|
|
942
|
+
? BigInt(RepositoryStand.processManagerVersion(loaded.entity))
|
|
943
|
+
: BigInt(loaded.current?.version?.number ?? 0);
|
|
944
|
+
const deferred = changed
|
|
945
|
+
? await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(options.tenantId, create(VersionSchema, { number: Number(version) }), lifecycle))
|
|
946
|
+
: undefined;
|
|
947
|
+
try {
|
|
948
|
+
const history = RepositoryStorage.historyConfiguration(this.#repository);
|
|
949
|
+
const outcome = await loaded.commits.commit({
|
|
950
|
+
context: loaded.storageInput.context,
|
|
951
|
+
entity: loaded.storageInput,
|
|
952
|
+
entityId,
|
|
953
|
+
...(loaded.current === undefined ? {} : { expected: loaded.current }),
|
|
954
|
+
next: EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle),
|
|
955
|
+
...(changed && history.stateHistory
|
|
956
|
+
? {
|
|
957
|
+
states: [
|
|
958
|
+
EntityRecords.pack(this.#repository.stateSchema, entityId, state, create(VersionSchema, { number: Number(version), timestamp: createdAt }), lifecycle),
|
|
959
|
+
],
|
|
960
|
+
}
|
|
961
|
+
: {}),
|
|
962
|
+
...(history.processManagerEventHistory
|
|
963
|
+
? {
|
|
964
|
+
diagnostics: events.map((event) => clone(EventSchema, event)),
|
|
965
|
+
}
|
|
966
|
+
: {}),
|
|
967
|
+
});
|
|
968
|
+
if (outcome !== "committed") {
|
|
969
|
+
deferred?.cancel();
|
|
970
|
+
throw new Error("Concurrent Process Manager state commit conflict.");
|
|
971
|
+
}
|
|
972
|
+
entityStateHistoryCaches.get(loaded.entity)?.clear();
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
deferred?.cancel();
|
|
976
|
+
throw error;
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
deferred?.notify();
|
|
980
|
+
}
|
|
981
|
+
catch (error) {
|
|
982
|
+
this.#runtime.recordDispatchFailure(events[events.length - 1] ?? create(EventSchema), error);
|
|
983
|
+
}
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
#defaultState(entityId) {
|
|
987
|
+
return create(this.#repository.stateSchema, {
|
|
988
|
+
[this.#repository.idField.localName]: entityId,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
class ProcessManagerCommandExecution {
|
|
993
|
+
#repository;
|
|
994
|
+
#routing;
|
|
995
|
+
#runtime;
|
|
996
|
+
#command;
|
|
997
|
+
#support;
|
|
998
|
+
constructor(repository, routing, runtime, command) {
|
|
999
|
+
this.#repository = repository;
|
|
1000
|
+
this.#routing = routing;
|
|
1001
|
+
this.#runtime = runtime;
|
|
1002
|
+
this.#command = command;
|
|
1003
|
+
this.#support = new ProcessManagerExecutionSupport(repository, runtime);
|
|
1004
|
+
}
|
|
1005
|
+
async run(replayedRoute) {
|
|
1006
|
+
RepositorySignals.requireCommandId(this.#command);
|
|
1007
|
+
const commandMessage = EntityInvocation.requireSignalMessage(this.#command.message, "command");
|
|
1008
|
+
const commandSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.commandSchemas, commandMessage.typeUrl, "command");
|
|
1009
|
+
const message = EntityInvocation.unpackRequired(commandMessage, commandSchema, "command");
|
|
1010
|
+
const route = replayedRoute ?? this.#repository.routeCommand(this.#command);
|
|
1011
|
+
const assignee = this.#routing.commandReadiness?.findCommandAssignee(route.messageFullTypeName);
|
|
1012
|
+
if (assignee === undefined) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const tenantOptions = RepositoryTenants.commandStandOptions(this.#runtime.context, this.#command);
|
|
1016
|
+
const loaded = await this.#support.load(route.entityId, tenantOptions);
|
|
1017
|
+
HandlerDispatchPublisher.command(this.#runtime, this.#repository, this.#command, route.entityId);
|
|
1018
|
+
let eventSignals;
|
|
1019
|
+
try {
|
|
1020
|
+
eventSignals = await this.#invoke(loaded.entity, assignee, message);
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
if (!RejectionThrowable.is(error)) {
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1026
|
+
return RepositorySignals.postRejectionEvent(this.#runtime, this.#repository, this.#command, route.entityId, error);
|
|
1027
|
+
}
|
|
1028
|
+
const events = this.#bindProducedEvents(eventSignals, route.entityId);
|
|
1029
|
+
const committed = await this.#support.commit(loaded, tenantOptions, RepositorySignals.executionTimestamp(), events);
|
|
1030
|
+
if (!committed)
|
|
1031
|
+
return undefined;
|
|
1032
|
+
if (RepositoryEntities.repositoryChanged(loaded.entity)) {
|
|
1033
|
+
EntityStateChangePublisher.command(this.#runtime, this.#repository, this.#command, route.entityId, loaded.current === undefined
|
|
1034
|
+
? undefined
|
|
1035
|
+
: EntityRecords.unpack(this.#repository.stateSchema, loaded.current).state, loaded.current === undefined
|
|
1036
|
+
? undefined
|
|
1037
|
+
: {
|
|
1038
|
+
archived: loaded.current.lifecycleFlags?.archived ?? false,
|
|
1039
|
+
deleted: loaded.current.lifecycleFlags?.deleted ?? false,
|
|
1040
|
+
}, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositoryStand.processManagerVersion(loaded.entity));
|
|
1041
|
+
}
|
|
1042
|
+
this.#postEvents(events);
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
async #invoke(entity, assignee, message) {
|
|
1046
|
+
transactionalEntityAccess.start(entity);
|
|
1047
|
+
try {
|
|
1048
|
+
const produced = await EntityInvocation.invokeEntityMethod(entity, assignee.handler.methodName, message, assignee.handler.parameterCount, EntityInvocation.commandHandlerContext(this.#command));
|
|
1049
|
+
const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
|
|
1050
|
+
if (commit.status === "rejected") {
|
|
1051
|
+
throw new TransitionValidationError(commit.validation.error);
|
|
1052
|
+
}
|
|
1053
|
+
return this.#support.normalizeProducedSignals(produced);
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
transactionalEntityAccess.rollback(entity);
|
|
1057
|
+
throw error;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
#bindProducedEvents(produced, entityId) {
|
|
1061
|
+
let sequence = 0;
|
|
1062
|
+
return Object.freeze(produced.map((signal) => {
|
|
1063
|
+
sequence += 1;
|
|
1064
|
+
return this.#bindProducedEvent(signal, entityId, sequence);
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
1067
|
+
#bindProducedEvent(signal, entityId, sequence) {
|
|
1068
|
+
const typeName = EntityInvocation.messageTypeName(signal);
|
|
1069
|
+
const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
|
|
1070
|
+
if (schema === undefined) {
|
|
1071
|
+
throw new Error(`Repository process-manager execution cannot pack event message "${typeName}".`);
|
|
1072
|
+
}
|
|
1073
|
+
const metadata = this.#runtime.signalMetadata.eventFromCommand(this.#command, sequence, {
|
|
1074
|
+
version: RepositoryStand.processManagerProducedVersion(sequence),
|
|
1075
|
+
});
|
|
1076
|
+
return create(EventSchema, {
|
|
1077
|
+
id: metadata.id,
|
|
1078
|
+
message: AnyMessages.pack(schema, signal),
|
|
1079
|
+
context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
#postEvents(events) {
|
|
1083
|
+
for (const event of events) {
|
|
1084
|
+
// spine-log-boundary: server.repository_event_follow_up
|
|
1085
|
+
void this.#runtime.postEventFollowUp(event).catch((error) => {
|
|
1086
|
+
this.#runtime.recordDispatchFailure(event, error);
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
class ProcessManagerEventExecution {
|
|
1092
|
+
#repository;
|
|
1093
|
+
#routing;
|
|
1094
|
+
#runtime;
|
|
1095
|
+
#event;
|
|
1096
|
+
#support;
|
|
1097
|
+
constructor(repository, routing, runtime, event) {
|
|
1098
|
+
this.#repository = repository;
|
|
1099
|
+
this.#routing = routing;
|
|
1100
|
+
this.#runtime = runtime;
|
|
1101
|
+
this.#event = event;
|
|
1102
|
+
this.#support = new ProcessManagerExecutionSupport(repository, runtime);
|
|
1103
|
+
}
|
|
1104
|
+
async run(acceptedRoute) {
|
|
1105
|
+
const intake = this.#readIntake(acceptedRoute);
|
|
1106
|
+
if (intake.reactors.length === 0 && intake.commanders.length === 0) {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
this.#validateSourceEventIdForFollowUps(intake);
|
|
1110
|
+
if (intake.route.entityIds.length === 1) {
|
|
1111
|
+
await InboxHandoff.handoffPmEvent(this.#repository, this.#runtime, this.#event, intake.route.entityIds[0]);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
await InboxHandoff.handoffPmEvents(this.#repository, this.#runtime, this.#event, intake.route.entityIds);
|
|
1115
|
+
}
|
|
1116
|
+
async runTarget(entityId, acceptedRoute) {
|
|
1117
|
+
const intake = this.#readIntake(acceptedRoute);
|
|
1118
|
+
if (intake.reactors.length === 0 && intake.commanders.length === 0) {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
this.#validateSourceEventIdForFollowUps(intake);
|
|
1122
|
+
await DispatchGuards.guardedEntityEventDispatch(this.#repository, this.#runtime, this.#event, entityId, async () => {
|
|
1123
|
+
await this.#executeEntity(entityId, intake);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
#readIntake(acceptedRoute) {
|
|
1127
|
+
const eventMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
|
|
1128
|
+
const eventSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.eventSchemas, eventMessage.typeUrl, "event");
|
|
1129
|
+
const message = EntityInvocation.unpackRequired(eventMessage, eventSchema, "event");
|
|
1130
|
+
const route = acceptedRoute;
|
|
1131
|
+
const reactors = this.#routing.eventReactors(route.messageFullTypeName, message, this.#event.context?.external === true);
|
|
1132
|
+
const commanders = this.#routing.commandReactions(route.messageFullTypeName, message, this.#event.context?.external === true);
|
|
1133
|
+
return Object.freeze({
|
|
1134
|
+
message,
|
|
1135
|
+
route,
|
|
1136
|
+
reactors: Object.freeze([...reactors]),
|
|
1137
|
+
commanders,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
async #executeEntity(entityId, intake) {
|
|
1141
|
+
const tenantOptions = RepositoryTenants.standTenantOptions(this.#runtime.context, this.#event);
|
|
1142
|
+
const loaded = await this.#support.load(entityId, tenantOptions);
|
|
1143
|
+
const produced = await this.#invokeHandlers(entityId, loaded.entity, intake);
|
|
1144
|
+
const events = this.#bindProducedEvents(produced.events, entityId);
|
|
1145
|
+
const diagnostics = [
|
|
1146
|
+
DispatchGuards.guardedJournalEvent(this.#repository, this.#event, entityId),
|
|
1147
|
+
...events,
|
|
1148
|
+
];
|
|
1149
|
+
const committed = await this.#support.commit(loaded, tenantOptions, this.#event.context?.timestamp ?? create(TimestampSchema), diagnostics);
|
|
1150
|
+
if (!committed)
|
|
1151
|
+
return;
|
|
1152
|
+
if (RepositoryEntities.repositoryChanged(loaded.entity)) {
|
|
1153
|
+
EntityStateChangePublisher.event(this.#runtime, this.#repository, this.#event, entityId, loaded.current === undefined
|
|
1154
|
+
? undefined
|
|
1155
|
+
: EntityRecords.unpack(this.#repository.stateSchema, loaded.current).state, loaded.current === undefined
|
|
1156
|
+
? undefined
|
|
1157
|
+
: {
|
|
1158
|
+
archived: loaded.current.lifecycleFlags?.archived ?? false,
|
|
1159
|
+
deleted: loaded.current.lifecycleFlags?.deleted ?? false,
|
|
1160
|
+
}, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositoryStand.processManagerVersion(loaded.entity));
|
|
1161
|
+
}
|
|
1162
|
+
this.#postEvents(events);
|
|
1163
|
+
await this.#postCommands(this.#bindProducedCommands(produced.commands));
|
|
1164
|
+
}
|
|
1165
|
+
#validateSourceEventIdForFollowUps(intake) {
|
|
1166
|
+
const emitsFollowUpEvents = intake.reactors.some((reactor) => RepositoryHandlers.handlerEmittedSchemas(reactor.handler).length > 0);
|
|
1167
|
+
if (!emitsFollowUpEvents && intake.commanders.length === 0) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
void this.#runtime.signalMetadata.originFromEvent(this.#event);
|
|
1171
|
+
}
|
|
1172
|
+
async #invokeHandlers(entityId, entity, intake) {
|
|
1173
|
+
const eventContext = EntityInvocation.eventHandlerContext(this.#event);
|
|
1174
|
+
const commands = [];
|
|
1175
|
+
const events = [];
|
|
1176
|
+
transactionalEntityAccess.start(entity);
|
|
1177
|
+
try {
|
|
1178
|
+
if (intake.reactors.length > 0) {
|
|
1179
|
+
HandlerDispatchPublisher.reactor(this.#runtime, this.#repository, this.#event, entityId);
|
|
1180
|
+
}
|
|
1181
|
+
for (const reactor of intake.reactors) {
|
|
1182
|
+
const produced = await EntityInvocation.invokeEntityMethod(entity, reactor.handler.methodName, intake.message, reactor.handler.parameterCount, eventContext);
|
|
1183
|
+
events.push(...this.#support.normalizeProducedSignals(produced));
|
|
1184
|
+
}
|
|
1185
|
+
for (const commander of intake.commanders) {
|
|
1186
|
+
const produced = await EntityInvocation.invokeEntityMethod(entity, commander.handler.methodName, intake.message, commander.handler.parameterCount, eventContext);
|
|
1187
|
+
commands.push(...this.#support.normalizeProducedSignals(produced));
|
|
1188
|
+
}
|
|
1189
|
+
const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
|
|
1190
|
+
if (commit.status === "rejected") {
|
|
1191
|
+
throw new TransitionValidationError(commit.validation.error);
|
|
1192
|
+
}
|
|
1193
|
+
return Object.freeze({
|
|
1194
|
+
commands: Object.freeze(commands),
|
|
1195
|
+
events: Object.freeze(events),
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
catch (error) {
|
|
1199
|
+
transactionalEntityAccess.rollback(entity);
|
|
1200
|
+
throw error;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
#bindProducedEvents(produced, entityId) {
|
|
1204
|
+
let sequence = 0;
|
|
1205
|
+
return Object.freeze(produced.map((signal) => {
|
|
1206
|
+
sequence += 1;
|
|
1207
|
+
return this.#bindProducedEvent(signal, entityId, sequence);
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
#bindProducedEvent(signal, entityId, sequence) {
|
|
1211
|
+
const typeName = EntityInvocation.messageTypeName(signal);
|
|
1212
|
+
const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
|
|
1213
|
+
if (schema === undefined) {
|
|
1214
|
+
throw new Error(`Repository process-manager execution cannot pack event message "${typeName}".`);
|
|
1215
|
+
}
|
|
1216
|
+
const metadata = this.#runtime.signalMetadata.eventFromEvent(this.#event, sequence, {
|
|
1217
|
+
version: RepositoryStand.processManagerProducedVersion(sequence),
|
|
1218
|
+
});
|
|
1219
|
+
return create(EventSchema, {
|
|
1220
|
+
id: metadata.id,
|
|
1221
|
+
message: AnyMessages.pack(schema, signal),
|
|
1222
|
+
context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
#bindProducedCommands(produced) {
|
|
1226
|
+
let sequence = 0;
|
|
1227
|
+
return Object.freeze(produced.map((signal) => {
|
|
1228
|
+
sequence += 1;
|
|
1229
|
+
const typeName = EntityInvocation.messageTypeName(signal);
|
|
1230
|
+
const schema = this.#routing.producedCommandSchemas.find((candidate) => candidate.typeName === typeName);
|
|
1231
|
+
if (schema === undefined) {
|
|
1232
|
+
throw new Error(`Repository process-manager execution cannot pack command message "${typeName}".`);
|
|
1233
|
+
}
|
|
1234
|
+
const metadata = this.#runtime.signalMetadata.commandFromEvent(this.#event, sequence);
|
|
1235
|
+
return create(CommandSchema, {
|
|
1236
|
+
id: metadata.id,
|
|
1237
|
+
message: AnyMessages.pack(schema, signal),
|
|
1238
|
+
context: metadata.context,
|
|
1239
|
+
});
|
|
1240
|
+
}));
|
|
1241
|
+
}
|
|
1242
|
+
async #postCommands(commands) {
|
|
1243
|
+
for (const command of commands) {
|
|
1244
|
+
await this.#runtime.onPostCommand(command);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
#postEvents(events) {
|
|
1248
|
+
for (const event of events) {
|
|
1249
|
+
// spine-log-boundary: server.process_manager_event_follow_up
|
|
1250
|
+
void this.#runtime.postEventFollowUp(event).catch((error) => {
|
|
1251
|
+
this.#runtime.recordDispatchFailure(event, error);
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Internal repository identity operations.
|
|
1258
|
+
*/
|
|
1259
|
+
const RepositoryIdentity = {
|
|
1260
|
+
createRepositorySnapshot(entityType, entityFamily, metadata) {
|
|
1261
|
+
const metadataCopy = RepositoryIdentity.cloneEntityMetadata(metadata);
|
|
1262
|
+
return Object.freeze({
|
|
1263
|
+
entityType,
|
|
1264
|
+
entityFamily,
|
|
1265
|
+
stateSchema: metadataCopy.schema,
|
|
1266
|
+
metadata: metadataCopy,
|
|
1267
|
+
stateFullTypeName: metadataCopy.fullTypeName,
|
|
1268
|
+
idField: RepositoryIdentity.cloneFieldMetadata(metadataCopy.idField),
|
|
1269
|
+
});
|
|
1270
|
+
},
|
|
1271
|
+
cloneRepositorySnapshot(snapshot) {
|
|
1272
|
+
const metadata = RepositoryIdentity.cloneEntityMetadata(snapshot.metadata);
|
|
1273
|
+
return Object.freeze({
|
|
1274
|
+
entityType: snapshot.entityType,
|
|
1275
|
+
entityFamily: snapshot.entityFamily,
|
|
1276
|
+
stateSchema: metadata.schema,
|
|
1277
|
+
metadata,
|
|
1278
|
+
stateFullTypeName: metadata.fullTypeName,
|
|
1279
|
+
idField: RepositoryIdentity.cloneFieldMetadata(metadata.idField),
|
|
1280
|
+
});
|
|
1281
|
+
},
|
|
1282
|
+
isRepositoryOptionsObject(options) {
|
|
1283
|
+
return typeof options === "object" && options !== null;
|
|
1284
|
+
},
|
|
1285
|
+
readEntityTypeOption(options) {
|
|
1286
|
+
try {
|
|
1287
|
+
return options.entityType;
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "Repository options entityType must be readable and resolve to a class constructor " +
|
|
1291
|
+
"extending Aggregate, Projection, or ProcessManager.");
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
readRepositorySchemaOption(options, entityTypeDisplayName, entityFamily) {
|
|
1295
|
+
try {
|
|
1296
|
+
return options.schema;
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
throw new RepositoryIdentityError("ENTITY_SCHEMA_KIND_MISMATCH", `Repository entity type "${entityTypeDisplayName}" is a ${entityFamily}, but ` +
|
|
1300
|
+
"the supplied state schema could not be read.");
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
isClassConstructor(entityType) {
|
|
1304
|
+
if (typeof entityType !== "function") {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
const source = Function.prototype.toString.call(entityType);
|
|
1309
|
+
return source.startsWith("class ");
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
/**
|
|
1316
|
+
* Resolves the runtime family of a repository-owned entity constructor.
|
|
1317
|
+
*
|
|
1318
|
+
* @internal
|
|
1319
|
+
*/
|
|
1320
|
+
resolveRepositoryEntityFamily(entityType) {
|
|
1321
|
+
if (typeof entityType !== "function" || !RepositoryIdentity.isClassConstructor(entityType)) {
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
const runtimeEntityType = entityType;
|
|
1325
|
+
if (RepositoryIdentity.hasEntityFamilyInheritance(runtimeEntityType, Aggregate, Aggregate.prototype)) {
|
|
1326
|
+
return "aggregate";
|
|
1327
|
+
}
|
|
1328
|
+
if (RepositoryIdentity.hasEntityFamilyInheritance(runtimeEntityType, Projection, Projection.prototype)) {
|
|
1329
|
+
return "projection";
|
|
1330
|
+
}
|
|
1331
|
+
if (RepositoryIdentity.hasEntityFamilyInheritance(runtimeEntityType, ProcessManager, ProcessManager.prototype)) {
|
|
1332
|
+
return "process-manager";
|
|
1333
|
+
}
|
|
1334
|
+
return undefined;
|
|
1335
|
+
},
|
|
1336
|
+
hasEntityFamilyInheritance(entityType, familyConstructor, familyPrototype) {
|
|
1337
|
+
try {
|
|
1338
|
+
return (Object.prototype.isPrototypeOf.call(familyConstructor, entityType) &&
|
|
1339
|
+
Object.prototype.isPrototypeOf.call(familyPrototype, entityType.prototype));
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
entityTypeName(entityType) {
|
|
1346
|
+
if ((typeof entityType !== "object" && typeof entityType !== "function") ||
|
|
1347
|
+
entityType === null) {
|
|
1348
|
+
return "(anonymous)";
|
|
1349
|
+
}
|
|
1350
|
+
const name = RepositoryIdentity.safeStringProperty(entityType, "name");
|
|
1351
|
+
return typeof name === "string" && name.length > 0 ? name : "(anonymous)";
|
|
1352
|
+
},
|
|
1353
|
+
safeStringProperty(value, propertyName) {
|
|
1354
|
+
try {
|
|
1355
|
+
const property = value[propertyName];
|
|
1356
|
+
return typeof property === "string" ? property : undefined;
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
return undefined;
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
describeRepositoryEntityMetadata(entityTypeDisplayName, entityFamily, schema) {
|
|
1363
|
+
try {
|
|
1364
|
+
return describeEntityMetadata(schema);
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
throw new RepositoryIdentityError("ENTITY_SCHEMA_KIND_MISMATCH", `Repository entity type "${entityTypeDisplayName}" is a ${entityFamily}, but ` +
|
|
1368
|
+
"the supplied state schema does not expose supported entity metadata.");
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
cloneEntityMetadata(metadata) {
|
|
1372
|
+
const idField = RepositoryIdentity.cloneFieldMetadata(metadata.idField);
|
|
1373
|
+
const firstFieldRoutingHint = Object.freeze({
|
|
1374
|
+
strategy: metadata.firstFieldRoutingHint.strategy,
|
|
1375
|
+
field: RepositoryIdentity.cloneFieldMetadata(metadata.firstFieldRoutingHint.field),
|
|
1376
|
+
});
|
|
1377
|
+
return Object.freeze({
|
|
1378
|
+
schema: metadata.schema,
|
|
1379
|
+
descriptor: metadata.descriptor,
|
|
1380
|
+
fullTypeName: metadata.fullTypeName,
|
|
1381
|
+
fileDescriptor: metadata.fileDescriptor,
|
|
1382
|
+
fileName: metadata.fileName,
|
|
1383
|
+
kind: metadata.kind,
|
|
1384
|
+
declaredVisibility: metadata.declaredVisibility,
|
|
1385
|
+
visibility: metadata.visibility,
|
|
1386
|
+
visibilitySource: metadata.visibilitySource,
|
|
1387
|
+
idField,
|
|
1388
|
+
firstFieldRoutingHint,
|
|
1389
|
+
columns: Object.freeze(metadata.columns.map((field) => RepositoryIdentity.cloneFieldMetadata(field))),
|
|
1390
|
+
setOnceFields: Object.freeze(metadata.setOnceFields.map((field) => RepositoryIdentity.cloneFieldMetadata(field))),
|
|
1391
|
+
});
|
|
1392
|
+
},
|
|
1393
|
+
cloneFieldMetadata(field) {
|
|
1394
|
+
return Object.freeze({
|
|
1395
|
+
descriptor: field.descriptor,
|
|
1396
|
+
name: field.name,
|
|
1397
|
+
localName: field.localName,
|
|
1398
|
+
jsonName: field.jsonName,
|
|
1399
|
+
number: field.number,
|
|
1400
|
+
});
|
|
1401
|
+
},
|
|
1402
|
+
};
|
|
1403
|
+
Object.freeze(RepositoryIdentity);
|
|
1404
|
+
/**
|
|
1405
|
+
* Internal entity invocation operations.
|
|
1406
|
+
*/
|
|
1407
|
+
const EntityInvocation = {
|
|
1408
|
+
isEventEnvelope(signal) {
|
|
1409
|
+
return (typeof signal === "object" &&
|
|
1410
|
+
signal !== null &&
|
|
1411
|
+
signal.$typeName === EventSchema.typeName);
|
|
1412
|
+
},
|
|
1413
|
+
messageTypeName(message) {
|
|
1414
|
+
const typeName = message.$typeName;
|
|
1415
|
+
if (typeof typeName !== "string" || typeName.length === 0) {
|
|
1416
|
+
throw new Error("Repository aggregate execution requires a generated event message.");
|
|
1417
|
+
}
|
|
1418
|
+
return typeName;
|
|
1419
|
+
},
|
|
1420
|
+
invokeEntityMethod(entity, methodName, message, parameterCount = 1, context) {
|
|
1421
|
+
const method = entity[methodName];
|
|
1422
|
+
if (typeof method !== "function") {
|
|
1423
|
+
throw new TypeError(`Repository entity execution requires method "${methodName}".`);
|
|
1424
|
+
}
|
|
1425
|
+
return Reflect.apply(method, entity, parameterCount === 2 ? [message, context] : [message]);
|
|
1426
|
+
},
|
|
1427
|
+
commandHandlerContext(command) {
|
|
1428
|
+
return command.context === undefined
|
|
1429
|
+
? create(CommandContextSchema)
|
|
1430
|
+
: clone(CommandContextSchema, command.context);
|
|
1431
|
+
},
|
|
1432
|
+
eventHandlerContext(event) {
|
|
1433
|
+
return event.context === undefined
|
|
1434
|
+
? create(EventContextSchema)
|
|
1435
|
+
: clone(EventContextSchema, event.context);
|
|
1436
|
+
},
|
|
1437
|
+
unpackRequired(message, schema, signalKind) {
|
|
1438
|
+
const unpacked = AnyMessages.unpack(message, schema);
|
|
1439
|
+
if (unpacked === undefined) {
|
|
1440
|
+
throw new Error(`Repository ${signalKind} execution requires a readable message.`);
|
|
1441
|
+
}
|
|
1442
|
+
return unpacked;
|
|
1443
|
+
},
|
|
1444
|
+
requireSignalMessage(message, signalKind) {
|
|
1445
|
+
if (message === undefined || message.typeUrl === "") {
|
|
1446
|
+
throw new Error(`Repository ${signalKind} execution requires message.typeUrl.`);
|
|
1447
|
+
}
|
|
1448
|
+
return message;
|
|
1449
|
+
},
|
|
1450
|
+
};
|
|
1451
|
+
Object.freeze(EntityInvocation);
|
|
1452
|
+
/**
|
|
1453
|
+
* Internal repository entities operations.
|
|
1454
|
+
*/
|
|
1455
|
+
const RepositoryEntities = {
|
|
1456
|
+
repositoryState(entity) {
|
|
1457
|
+
return entity.state;
|
|
1458
|
+
},
|
|
1459
|
+
repositoryLifecycle(entity) {
|
|
1460
|
+
return entity.lifecycle;
|
|
1461
|
+
},
|
|
1462
|
+
repositoryChanged(entity) {
|
|
1463
|
+
return entity.changed === true;
|
|
1464
|
+
},
|
|
1465
|
+
};
|
|
1466
|
+
Object.freeze(RepositoryEntities);
|
|
1467
|
+
/**
|
|
1468
|
+
* Internal repository signals operations.
|
|
1469
|
+
*/
|
|
1470
|
+
const RepositorySignals = {
|
|
1471
|
+
readEventVersion(event) {
|
|
1472
|
+
const number = event.context?.version?.number;
|
|
1473
|
+
if (number === undefined) {
|
|
1474
|
+
throw new Error("Repository aggregate execution requires readable event versions.");
|
|
1475
|
+
}
|
|
1476
|
+
return BigInt(number);
|
|
1477
|
+
},
|
|
1478
|
+
eventVersionNumber(version) {
|
|
1479
|
+
if (version > 2147483647n || version < -2147483648n) {
|
|
1480
|
+
throw new Error("Repository aggregate execution requires versions in the protobuf int32 range.");
|
|
1481
|
+
}
|
|
1482
|
+
return Number(version);
|
|
1483
|
+
},
|
|
1484
|
+
eventContextWithProducer(context, repository, entityId) {
|
|
1485
|
+
const producerId = EntityIds.pack(repository.stateSchema, entityId);
|
|
1486
|
+
return create(EventContextSchema, { ...context, producerId });
|
|
1487
|
+
},
|
|
1488
|
+
postRejectionEvent(runtime, repository, command, entityId, rejection) {
|
|
1489
|
+
runtime.registerEventSchema(rejection.schema);
|
|
1490
|
+
const metadata = runtime.signalMetadata.eventFromCommand(command, 1, {});
|
|
1491
|
+
const event = create(EventSchema, {
|
|
1492
|
+
id: metadata.id,
|
|
1493
|
+
message: AnyMessages.pack(rejection.schema, rejection.messageThrown()),
|
|
1494
|
+
context: create(EventContextSchema, {
|
|
1495
|
+
...RepositorySignals.eventContextWithProducer(metadata.context, repository, entityId),
|
|
1496
|
+
rejection: create(RejectionEventContextSchema, {
|
|
1497
|
+
command: clone(CommandSchema, command),
|
|
1498
|
+
stacktrace: rejection.stack ?? "",
|
|
1499
|
+
}),
|
|
1500
|
+
}),
|
|
1501
|
+
});
|
|
1502
|
+
return async () => {
|
|
1503
|
+
try {
|
|
1504
|
+
// spine-log-boundary: server.repository_rejection_follow_up
|
|
1505
|
+
await runtime.postEventFollowUp(event);
|
|
1506
|
+
}
|
|
1507
|
+
catch (error) {
|
|
1508
|
+
runtime.recordDispatchFailure(event, error);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
},
|
|
1512
|
+
requireCommandId(command) {
|
|
1513
|
+
if (command.id === undefined || command.id.uuid.trim().length === 0) {
|
|
1514
|
+
throw new Error("Repository aggregate execution requires command.id to bind event origins.");
|
|
1515
|
+
}
|
|
1516
|
+
return command.id;
|
|
1517
|
+
},
|
|
1518
|
+
requireEventId(event) {
|
|
1519
|
+
if (event.id === undefined || event.id.value.trim().length === 0) {
|
|
1520
|
+
throw new Error("Repository projection inbox handoff requires event.id.");
|
|
1521
|
+
}
|
|
1522
|
+
return event.id;
|
|
1523
|
+
},
|
|
1524
|
+
executionTimestamp() {
|
|
1525
|
+
const milliseconds = Date.now();
|
|
1526
|
+
return create(TimestampSchema, {
|
|
1527
|
+
seconds: BigInt(Math.floor(milliseconds / 1_000)),
|
|
1528
|
+
nanos: (milliseconds % 1_000) * 1_000_000,
|
|
1529
|
+
});
|
|
1530
|
+
},
|
|
1531
|
+
};
|
|
1532
|
+
/**
|
|
1533
|
+
* Builds and best-effort dispatches committed entity state notifications.
|
|
1534
|
+
*/
|
|
1535
|
+
class EntityStateChangePublishing {
|
|
1536
|
+
command(runtime, repository, command, entityId, oldState, oldLifecycle, newState, lifecycle, version) {
|
|
1537
|
+
this.#publish(runtime, (ordinal) => runtime.signalMetadata.eventFromCommand(command, ordinal, {
|
|
1538
|
+
version,
|
|
1539
|
+
}), {
|
|
1540
|
+
id: AnyMessages.pack(CommandIdSchema, command.id),
|
|
1541
|
+
typeUrl: command.message?.typeUrl ?? "",
|
|
1542
|
+
}, { repository, entityId, oldState, oldLifecycle, newState, lifecycle, version });
|
|
1543
|
+
}
|
|
1544
|
+
event(runtime, repository, source, entityId, oldState, oldLifecycle, newState, lifecycle, version) {
|
|
1545
|
+
this.#publish(runtime, (ordinal) => runtime.signalMetadata.eventFromEvent(source, ordinal, {
|
|
1546
|
+
version,
|
|
1547
|
+
}), {
|
|
1548
|
+
id: AnyMessages.pack(EventIdSchema, source.id),
|
|
1549
|
+
typeUrl: source.message?.typeUrl ?? "",
|
|
1550
|
+
}, { repository, entityId, oldState, oldLifecycle, newState, lifecycle, version });
|
|
1551
|
+
}
|
|
1552
|
+
#publish(runtime, metadataFor, origin, change) {
|
|
1553
|
+
const drafts = this.#drafts(origin, change);
|
|
1554
|
+
drafts.forEach((draft, ordinal) => {
|
|
1555
|
+
const metadata = metadataFor(ordinal);
|
|
1556
|
+
runtime.registerSystemEventSchema(draft.schema);
|
|
1557
|
+
const event = create(EventSchema, {
|
|
1558
|
+
id: metadata.id,
|
|
1559
|
+
message: AnyMessages.pack(draft.schema, draft.messageAt(metadata.context.timestamp)),
|
|
1560
|
+
context: RepositorySignals.eventContextWithProducer(metadata.context, change.repository, change.entityId),
|
|
1561
|
+
});
|
|
1562
|
+
this.#post(runtime, event);
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
#drafts(origin, change) {
|
|
1566
|
+
const fields = this.#fields(origin, change);
|
|
1567
|
+
const archive = this.#archiveDraft(fields, change);
|
|
1568
|
+
const deletion = this.#deleteDraft(fields, change);
|
|
1569
|
+
return [...this.#stateDrafts(fields, change), archive, deletion].filter((draft) => draft !== undefined);
|
|
1570
|
+
}
|
|
1571
|
+
#fields(origin, change) {
|
|
1572
|
+
const entity = create(MessageIdSchema, {
|
|
1573
|
+
id: this.#packEntityId(change.repository, change.entityId),
|
|
1574
|
+
typeUrl: TypeUrls.derive(change.repository.stateSchema),
|
|
1575
|
+
});
|
|
1576
|
+
return {
|
|
1577
|
+
entity,
|
|
1578
|
+
signalId: [create(MessageIdSchema, { id: origin.id, typeUrl: origin.typeUrl })],
|
|
1579
|
+
state: AnyMessages.pack(change.repository.stateSchema, change.newState),
|
|
1580
|
+
version: create(VersionSchema, { number: change.version }),
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
#stateDrafts(fields, change) {
|
|
1584
|
+
const drafts = [];
|
|
1585
|
+
if (change.oldState === undefined) {
|
|
1586
|
+
drafts.push({
|
|
1587
|
+
schema: EntityLog.EntityCreatedSchema,
|
|
1588
|
+
messageAt: () => create(EntityLog.EntityCreatedSchema, {
|
|
1589
|
+
entity: fields.entity,
|
|
1590
|
+
kind: this.#kind(change.repository.metadata.kind),
|
|
1591
|
+
}),
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
if (change.oldState === undefined ||
|
|
1595
|
+
!this.#sameState(change.repository.stateSchema, change.oldState, change.newState)) {
|
|
1596
|
+
drafts.push(this.#stateChangedDraft(fields, change));
|
|
1597
|
+
}
|
|
1598
|
+
return drafts;
|
|
1599
|
+
}
|
|
1600
|
+
#stateChangedDraft(fields, change) {
|
|
1601
|
+
return {
|
|
1602
|
+
schema: EntityLog.EntityStateChangedSchema,
|
|
1603
|
+
messageAt: (when) => create(EntityLog.EntityStateChangedSchema, {
|
|
1604
|
+
entity: fields.entity,
|
|
1605
|
+
...(change.oldState === undefined
|
|
1606
|
+
? {}
|
|
1607
|
+
: {
|
|
1608
|
+
oldState: AnyMessages.pack(change.repository.stateSchema, change.oldState),
|
|
1609
|
+
}),
|
|
1610
|
+
newState: fields.state,
|
|
1611
|
+
signalId: fields.signalId,
|
|
1612
|
+
when,
|
|
1613
|
+
newVersion: fields.version,
|
|
1614
|
+
}),
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
#archiveDraft(fields, change) {
|
|
1618
|
+
const previous = change.oldLifecycle ?? { archived: false, deleted: false };
|
|
1619
|
+
if (previous.archived === change.lifecycle.archived)
|
|
1620
|
+
return undefined;
|
|
1621
|
+
return change.lifecycle.archived
|
|
1622
|
+
? {
|
|
1623
|
+
schema: EntityLog.EntityArchivedSchema,
|
|
1624
|
+
messageAt: (when) => create(EntityLog.EntityArchivedSchema, {
|
|
1625
|
+
entity: fields.entity,
|
|
1626
|
+
signalId: fields.signalId,
|
|
1627
|
+
when,
|
|
1628
|
+
version: fields.version,
|
|
1629
|
+
lastState: fields.state,
|
|
1630
|
+
}),
|
|
1631
|
+
}
|
|
1632
|
+
: {
|
|
1633
|
+
schema: EntityLog.EntityUnarchivedSchema,
|
|
1634
|
+
messageAt: (when) => create(EntityLog.EntityUnarchivedSchema, {
|
|
1635
|
+
entity: fields.entity,
|
|
1636
|
+
signalId: fields.signalId,
|
|
1637
|
+
when,
|
|
1638
|
+
version: fields.version,
|
|
1639
|
+
state: fields.state,
|
|
1640
|
+
}),
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
#deleteDraft(fields, change) {
|
|
1644
|
+
const previous = change.oldLifecycle ?? { archived: false, deleted: false };
|
|
1645
|
+
if (previous.deleted === change.lifecycle.deleted)
|
|
1646
|
+
return undefined;
|
|
1647
|
+
return change.lifecycle.deleted
|
|
1648
|
+
? {
|
|
1649
|
+
schema: EntityLog.EntityDeletedSchema,
|
|
1650
|
+
messageAt: (when) => create(EntityLog.EntityDeletedSchema, {
|
|
1651
|
+
entity: fields.entity,
|
|
1652
|
+
signalId: fields.signalId,
|
|
1653
|
+
when,
|
|
1654
|
+
version: fields.version,
|
|
1655
|
+
deletion: { case: "markedAsDeleted", value: true },
|
|
1656
|
+
lastState: fields.state,
|
|
1657
|
+
}),
|
|
1658
|
+
}
|
|
1659
|
+
: {
|
|
1660
|
+
schema: EntityLog.EntityRestoredSchema,
|
|
1661
|
+
messageAt: (when) => create(EntityLog.EntityRestoredSchema, {
|
|
1662
|
+
entity: fields.entity,
|
|
1663
|
+
signalId: fields.signalId,
|
|
1664
|
+
when,
|
|
1665
|
+
version: fields.version,
|
|
1666
|
+
state: fields.state,
|
|
1667
|
+
}),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
#post(runtime, event) {
|
|
1671
|
+
try {
|
|
1672
|
+
// spine-log-boundary: server.repository_system_follow_up
|
|
1673
|
+
void runtime.postSystemFollowUp(event).catch((error) => {
|
|
1674
|
+
runtime.recordDispatchFailure(event, error);
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
catch (error) {
|
|
1678
|
+
runtime.recordDispatchFailure(event, error);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
#sameState(schema, left, right) {
|
|
1682
|
+
const leftBytes = toBinary(schema, left);
|
|
1683
|
+
const rightBytes = toBinary(schema, right);
|
|
1684
|
+
return (leftBytes.length === rightBytes.length &&
|
|
1685
|
+
leftBytes.every((value, index) => value === rightBytes[index]));
|
|
1686
|
+
}
|
|
1687
|
+
#kind(kind) {
|
|
1688
|
+
return kind === "aggregate"
|
|
1689
|
+
? EntityOption_Kind.AGGREGATE
|
|
1690
|
+
: kind === "projection"
|
|
1691
|
+
? EntityOption_Kind.PROJECTION
|
|
1692
|
+
: EntityOption_Kind.PROCESS_MANAGER;
|
|
1693
|
+
}
|
|
1694
|
+
#packEntityId(repository, entityId) {
|
|
1695
|
+
return EntityIds.pack(repository.stateSchema, entityId);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const EntityStateChangePublisher = Object.freeze(new EntityStateChangePublishing());
|
|
1699
|
+
/**
|
|
1700
|
+
* Builds and best-effort dispatches accepted handler diagnostics.
|
|
1701
|
+
*/
|
|
1702
|
+
class HandlerDispatchPublishing {
|
|
1703
|
+
command(runtime, repository, command, entityId) {
|
|
1704
|
+
try {
|
|
1705
|
+
const context = runtime.signalMetadata.eventContext({
|
|
1706
|
+
origin: runtime.signalMetadata.originFromCommand(command),
|
|
1707
|
+
});
|
|
1708
|
+
const event = create(EventSchema, {
|
|
1709
|
+
id: runtime.signalMetadata.eventId(),
|
|
1710
|
+
message: AnyMessages.pack(EntityLog.CommandDispatchedToHandlerSchema, this.#message(repository, command, entityId, context.timestamp)),
|
|
1711
|
+
context,
|
|
1712
|
+
});
|
|
1713
|
+
this.#post(runtime, EntityLog.CommandDispatchedToHandlerSchema, event);
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
runtime.recordDispatchFailure(create(EventSchema), error);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
subscriber(runtime, repository, event, entityId) {
|
|
1720
|
+
this.#publishEvent(runtime, repository, event, entityId, EntityLog.EventDispatchedToSubscriberSchema);
|
|
1721
|
+
}
|
|
1722
|
+
reactor(runtime, repository, event, entityId) {
|
|
1723
|
+
this.#publishEvent(runtime, repository, event, entityId, EntityLog.EventDispatchedToReactorSchema);
|
|
1724
|
+
}
|
|
1725
|
+
#publishEvent(runtime, repository, event, entityId, schema) {
|
|
1726
|
+
try {
|
|
1727
|
+
const context = runtime.signalMetadata.eventContext({
|
|
1728
|
+
origin: runtime.signalMetadata.originFromEvent(event),
|
|
1729
|
+
});
|
|
1730
|
+
const diagnostic = create(EventSchema, {
|
|
1731
|
+
id: runtime.signalMetadata.eventId(),
|
|
1732
|
+
message: AnyMessages.pack(schema, this.#eventMessage(schema, repository, event, entityId, context.timestamp)),
|
|
1733
|
+
context,
|
|
1734
|
+
});
|
|
1735
|
+
this.#post(runtime, schema, diagnostic);
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
runtime.recordDispatchFailure(create(EventSchema), error);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
#message(repository, command, entityId, whenDispatched) {
|
|
1742
|
+
return create(EntityLog.CommandDispatchedToHandlerSchema, {
|
|
1743
|
+
receiver: create(MessageIdSchema, {
|
|
1744
|
+
id: this.#packEntityId(repository, entityId),
|
|
1745
|
+
typeUrl: TypeUrls.derive(repository.stateSchema),
|
|
1746
|
+
}),
|
|
1747
|
+
payload: clone(CommandSchema, command),
|
|
1748
|
+
whenDispatched,
|
|
1749
|
+
entityType: create(EntityTypeNameSchema, {
|
|
1750
|
+
impl: { case: "javaClassName", value: repository.entityType.name },
|
|
1751
|
+
}),
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
#post(runtime, schema, event) {
|
|
1755
|
+
try {
|
|
1756
|
+
runtime.registerSystemEventSchema(schema);
|
|
1757
|
+
// spine-log-boundary: server.repository_system_dispatch_follow_up
|
|
1758
|
+
void runtime.postSystemFollowUp(event).catch((error) => {
|
|
1759
|
+
runtime.recordDispatchFailure(event, error);
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
catch (error) {
|
|
1763
|
+
runtime.recordDispatchFailure(event, error);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
#packEntityId(repository, entityId) {
|
|
1767
|
+
return EntityIds.pack(repository.stateSchema, entityId);
|
|
1768
|
+
}
|
|
1769
|
+
#eventMessage(schema, repository, event, entityId, whenDispatched) {
|
|
1770
|
+
const fields = {
|
|
1771
|
+
receiver: create(MessageIdSchema, {
|
|
1772
|
+
id: this.#packEntityId(repository, entityId),
|
|
1773
|
+
typeUrl: TypeUrls.derive(repository.stateSchema),
|
|
1774
|
+
}),
|
|
1775
|
+
payload: clone(EventSchema, event),
|
|
1776
|
+
whenDispatched,
|
|
1777
|
+
entityType: create(EntityTypeNameSchema, {
|
|
1778
|
+
impl: { case: "javaClassName", value: repository.entityType.name },
|
|
1779
|
+
}),
|
|
1780
|
+
};
|
|
1781
|
+
return schema === EntityLog.EventDispatchedToSubscriberSchema
|
|
1782
|
+
? create(EntityLog.EventDispatchedToSubscriberSchema, fields)
|
|
1783
|
+
: create(EntityLog.EventDispatchedToReactorSchema, fields);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const HandlerDispatchPublisher = Object.freeze(new HandlerDispatchPublishing());
|
|
1787
|
+
Object.freeze(RepositorySignals);
|
|
1788
|
+
/**
|
|
1789
|
+
* Internal repository stand operations.
|
|
1790
|
+
*/
|
|
1791
|
+
const RepositoryStand = {
|
|
1792
|
+
standUpdateOptions(tenantId, version, lifecycle) {
|
|
1793
|
+
return Object.freeze({
|
|
1794
|
+
...(tenantId === undefined ? {} : { tenantId }),
|
|
1795
|
+
...(version === undefined ? {} : { version }),
|
|
1796
|
+
lifecycle,
|
|
1797
|
+
});
|
|
1798
|
+
},
|
|
1799
|
+
projectionVersion(version) {
|
|
1800
|
+
return typeof version === "bigint" ? Number(version) : (version?.number ?? 0);
|
|
1801
|
+
},
|
|
1802
|
+
processManagerVersion(entity) {
|
|
1803
|
+
const version = entity.version;
|
|
1804
|
+
return typeof version === "number" ? version + 1 : 1;
|
|
1805
|
+
},
|
|
1806
|
+
processManagerProducedVersion(sequence) {
|
|
1807
|
+
return sequence;
|
|
1808
|
+
},
|
|
1809
|
+
};
|
|
1810
|
+
Object.freeze(RepositoryStand);
|
|
1811
|
+
/**
|
|
1812
|
+
* Internal repository tenants operations.
|
|
1813
|
+
*/
|
|
1814
|
+
const RepositoryTenants = {
|
|
1815
|
+
entityInboxDeliveryContext(context, tenantId) {
|
|
1816
|
+
if (!context.multitenant) {
|
|
1817
|
+
return Object.freeze({ name: context.name, multitenant: false });
|
|
1818
|
+
}
|
|
1819
|
+
const tid = tenantId;
|
|
1820
|
+
if (tid === undefined) {
|
|
1821
|
+
throw new Error(`Multitenant Entity Inbox handoff for "${context.name}" requires tenantId.`);
|
|
1822
|
+
}
|
|
1823
|
+
return Object.freeze({
|
|
1824
|
+
name: context.name,
|
|
1825
|
+
multitenant: true,
|
|
1826
|
+
tenantId: RepositoryTenants.require(tid),
|
|
1827
|
+
});
|
|
1828
|
+
},
|
|
1829
|
+
projectionDeliveryContext(context, tenantId) {
|
|
1830
|
+
if (!context.multitenant) {
|
|
1831
|
+
return Object.freeze({ name: context.name, multitenant: false });
|
|
1832
|
+
}
|
|
1833
|
+
const tid = tenantId;
|
|
1834
|
+
if (tid === undefined) {
|
|
1835
|
+
throw new Error(`Multitenant projection inbox handoff for "${context.name}" requires tenantId.`);
|
|
1836
|
+
}
|
|
1837
|
+
return Object.freeze({
|
|
1838
|
+
name: context.name,
|
|
1839
|
+
multitenant: true,
|
|
1840
|
+
tenantId: RepositoryTenants.require(tid),
|
|
1841
|
+
});
|
|
1842
|
+
},
|
|
1843
|
+
requireProjectionTenant(context, event) {
|
|
1844
|
+
if (!context.multitenant) {
|
|
1845
|
+
return undefined;
|
|
1846
|
+
}
|
|
1847
|
+
const tenantId = RepositoryTenants.readEventTenant(event);
|
|
1848
|
+
if (tenantId === undefined) {
|
|
1849
|
+
throw new Error(`Multitenant projection inbox handoff for "${context.name}" requires tenantId.`);
|
|
1850
|
+
}
|
|
1851
|
+
return RepositoryTenants.require(tenantId);
|
|
1852
|
+
},
|
|
1853
|
+
requirePmEventTenant(context, event) {
|
|
1854
|
+
if (!context.multitenant) {
|
|
1855
|
+
return undefined;
|
|
1856
|
+
}
|
|
1857
|
+
const tenantId = RepositoryTenants.readEventTenant(event);
|
|
1858
|
+
if (tenantId === undefined) {
|
|
1859
|
+
throw new Error(`Multitenant Entity Inbox handoff for "${context.name}" requires tenantId.`);
|
|
1860
|
+
}
|
|
1861
|
+
return RepositoryTenants.require(tenantId);
|
|
1862
|
+
},
|
|
1863
|
+
requireCommandTenant(context, command) {
|
|
1864
|
+
if (!context.multitenant) {
|
|
1865
|
+
return undefined;
|
|
1866
|
+
}
|
|
1867
|
+
const tenantId = RepositoryTenants.readCommandTenant(command);
|
|
1868
|
+
if (tenantId === undefined) {
|
|
1869
|
+
throw new Error(`Multitenant Entity Inbox handoff for "${context.name}" requires tenantId.`);
|
|
1870
|
+
}
|
|
1871
|
+
return RepositoryTenants.require(tenantId);
|
|
1872
|
+
},
|
|
1873
|
+
storageContextForCommand(context, command) {
|
|
1874
|
+
if (!context.multitenant) {
|
|
1875
|
+
return Object.freeze({ name: context.name, multitenant: false });
|
|
1876
|
+
}
|
|
1877
|
+
const tenantId = RepositoryTenants.readCommandTenant(command);
|
|
1878
|
+
if (tenantId === undefined)
|
|
1879
|
+
throw new Error(`Multitenant command for "${context.name}" requires tenantId.`);
|
|
1880
|
+
return Object.freeze({
|
|
1881
|
+
name: context.name,
|
|
1882
|
+
multitenant: true,
|
|
1883
|
+
tenantId: RepositoryTenants.require(tenantId),
|
|
1884
|
+
});
|
|
1885
|
+
},
|
|
1886
|
+
storageContextForEvent(context, event) {
|
|
1887
|
+
if (!context.multitenant) {
|
|
1888
|
+
return Object.freeze({ name: context.name, multitenant: false });
|
|
1889
|
+
}
|
|
1890
|
+
const tenantId = RepositoryTenants.readEventTenant(event);
|
|
1891
|
+
if (tenantId === undefined)
|
|
1892
|
+
throw new Error(`Multitenant event for "${context.name}" requires tenantId.`);
|
|
1893
|
+
return Object.freeze({
|
|
1894
|
+
name: context.name,
|
|
1895
|
+
multitenant: true,
|
|
1896
|
+
tenantId: RepositoryTenants.require(tenantId),
|
|
1897
|
+
});
|
|
1898
|
+
},
|
|
1899
|
+
storageContextForTenant(context, tenantId) {
|
|
1900
|
+
if (!context.multitenant)
|
|
1901
|
+
return Object.freeze({ name: context.name, multitenant: false });
|
|
1902
|
+
if (tenantId === undefined)
|
|
1903
|
+
throw new Error(`Multitenant storage for "${context.name}" requires tenantId.`);
|
|
1904
|
+
return Object.freeze({
|
|
1905
|
+
name: context.name,
|
|
1906
|
+
multitenant: true,
|
|
1907
|
+
tenantId: RepositoryTenants.require(tenantId),
|
|
1908
|
+
});
|
|
1909
|
+
},
|
|
1910
|
+
readCommandTenant(command) {
|
|
1911
|
+
return RepositoryTenants.tenantValue(command.context?.actorContext?.tenantId);
|
|
1912
|
+
},
|
|
1913
|
+
standTenantOptions(context, event) {
|
|
1914
|
+
if (!context.multitenant) {
|
|
1915
|
+
return {};
|
|
1916
|
+
}
|
|
1917
|
+
const tenantId = RepositoryTenants.readEventTenant(event);
|
|
1918
|
+
return tenantId === undefined ? {} : { tenantId };
|
|
1919
|
+
},
|
|
1920
|
+
commandStandOptions(context, command) {
|
|
1921
|
+
if (!context.multitenant) {
|
|
1922
|
+
return {};
|
|
1923
|
+
}
|
|
1924
|
+
const tenantId = RepositoryTenants.readCommandTenant(command);
|
|
1925
|
+
return tenantId === undefined ? {} : { tenantId };
|
|
1926
|
+
},
|
|
1927
|
+
readEventTenant(event) {
|
|
1928
|
+
switch (event.context?.origin.case) {
|
|
1929
|
+
case "importContext":
|
|
1930
|
+
return RepositoryTenants.tenantValue(event.context.origin.value.tenantId);
|
|
1931
|
+
case "pastMessage":
|
|
1932
|
+
return RepositoryTenants.tenantValue(event.context.origin.value.actorContext?.tenantId);
|
|
1933
|
+
default:
|
|
1934
|
+
return undefined;
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
tenantValue(tenantId) {
|
|
1938
|
+
return tenantId === undefined ? undefined : clone(TenantIdSchema, tenantId);
|
|
1939
|
+
},
|
|
1940
|
+
require(tenantId) {
|
|
1941
|
+
return TenantBoundary.from(tenantId).tenantId;
|
|
1942
|
+
},
|
|
1943
|
+
equal(left, right) {
|
|
1944
|
+
return TenantBoundary.from(left).key === TenantBoundary.from(right).key;
|
|
1945
|
+
},
|
|
1946
|
+
};
|
|
1947
|
+
Object.freeze(RepositoryTenants);
|
|
1948
|
+
/**
|
|
1949
|
+
* Internal repository handlers operations.
|
|
1950
|
+
*/
|
|
1951
|
+
const RepositoryHandlers = {
|
|
1952
|
+
normalizeHandlers(handlersOption) {
|
|
1953
|
+
if (handlersOption === undefined) {
|
|
1954
|
+
return Object.freeze([]);
|
|
1955
|
+
}
|
|
1956
|
+
if (RepositoryHandlers.isHandlersArray(handlersOption)) {
|
|
1957
|
+
return Object.freeze([...handlersOption]);
|
|
1958
|
+
}
|
|
1959
|
+
return Object.freeze([handlersOption]);
|
|
1960
|
+
},
|
|
1961
|
+
createCommandReactionMap(handlers) {
|
|
1962
|
+
const byEvent = new Map();
|
|
1963
|
+
const registry = new HandlerMetadataRegistry(handlers);
|
|
1964
|
+
for (const entry of registry.findHandlersByKind("command-reaction")) {
|
|
1965
|
+
if (RepositoryHandlers.handlerEmittedSchemas(entry.handler).length === 0) {
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1968
|
+
RepositoryHandlers.pushMapValue(byEvent, entry.handler.messageFullTypeName, entry);
|
|
1969
|
+
}
|
|
1970
|
+
return byEvent;
|
|
1971
|
+
},
|
|
1972
|
+
createEventFilterPlans(byEvent, stringifiers) {
|
|
1973
|
+
const plans = new Map();
|
|
1974
|
+
for (const [eventType, values] of byEvent) {
|
|
1975
|
+
plans.set(eventType, EventHandlerFilters.compile(values.map((value) => ({
|
|
1976
|
+
value,
|
|
1977
|
+
schema: value.handler.schema,
|
|
1978
|
+
...(value.handler.where === undefined ? {} : { where: value.handler.where }),
|
|
1979
|
+
})), stringifiers));
|
|
1980
|
+
}
|
|
1981
|
+
return plans;
|
|
1982
|
+
},
|
|
1983
|
+
forOrigin(values, external) {
|
|
1984
|
+
return Object.freeze(values.filter((value) => (value.handler.origin === "external") === external));
|
|
1985
|
+
},
|
|
1986
|
+
readinessMap(schemas, find) {
|
|
1987
|
+
return new Map(schemas.map((schema) => [schema.typeName, find(schema.typeName)]));
|
|
1988
|
+
},
|
|
1989
|
+
handlerEmittedSchemas(handler) {
|
|
1990
|
+
return HandlerMetadataValues.emittedSchemas(handler);
|
|
1991
|
+
},
|
|
1992
|
+
isHandlersArray(value) {
|
|
1993
|
+
return Array.isArray(value);
|
|
1994
|
+
},
|
|
1995
|
+
validateHandlers(entityType, metadata, handlers) {
|
|
1996
|
+
for (const handlersMetadata of handlers) {
|
|
1997
|
+
if (!HandlerMetadataValues.isAuthentic(handlersMetadata) ||
|
|
1998
|
+
handlersMetadata.entityType !== entityType ||
|
|
1999
|
+
handlersMetadata.entity.fullTypeName !== metadata.fullTypeName) {
|
|
2000
|
+
throw new RepositoryIdentityError("ENTITY_SCHEMA_KIND_MISMATCH", `Repository entity type "${entityType.name}" does not match the supplied handler metadata.`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
uniqueSchemas(schemas) {
|
|
2005
|
+
const byTypeUrl = new Map();
|
|
2006
|
+
for (const schema of schemas) {
|
|
2007
|
+
byTypeUrl.set(TypeUrls.derive(schema), schema);
|
|
2008
|
+
}
|
|
2009
|
+
return Object.freeze([...byTypeUrl.values()]);
|
|
2010
|
+
},
|
|
2011
|
+
pushMapValue(map, key, value) {
|
|
2012
|
+
const values = map.get(key);
|
|
2013
|
+
if (values === undefined) {
|
|
2014
|
+
map.set(key, [value]);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
values.push(value);
|
|
2018
|
+
},
|
|
2019
|
+
};
|
|
2020
|
+
Object.freeze(RepositoryHandlers);
|
|
2021
|
+
/**
|
|
2022
|
+
* Internal repository routes operations.
|
|
2023
|
+
*/
|
|
2024
|
+
const RepositoryRoutes = {
|
|
2025
|
+
createRepositoryRouting(entityType, entityFamily, metadata, handlersOption, producedEvents, commandRouting, eventRouting, stateUpdateRouting, stringifiers) {
|
|
2026
|
+
const handlers = RepositoryHandlers.normalizeHandlers(handlersOption);
|
|
2027
|
+
RepositoryHandlers.validateHandlers(entityType, metadata, handlers);
|
|
2028
|
+
const commandReadiness = handlers.length === 0 ? undefined : CommandRegistrationReadiness.fromEntityHandlers(handlers);
|
|
2029
|
+
const eventReadiness = handlers.length === 0 ? undefined : EventRegistrationReadiness.fromEntityHandlers(handlers);
|
|
2030
|
+
const commandSchemas = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.commandAssignments.map((assignment) => assignment.schema)));
|
|
2031
|
+
const eventSchemas = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => [
|
|
2032
|
+
...handler.commandReactions.map((reaction) => reaction.schema),
|
|
2033
|
+
...handler.eventSubscriptions.map((subscription) => subscription.schema),
|
|
2034
|
+
...handler.eventReactions.map((reaction) => reaction.schema),
|
|
2035
|
+
// Applications remain valid routing metadata. Aggregate persistence does
|
|
2036
|
+
// not invoke them to reconstruct or mutate state.
|
|
2037
|
+
...handler.eventApplications.map((application) => application.schema),
|
|
2038
|
+
]));
|
|
2039
|
+
const externalEventSchemas = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.handlers
|
|
2040
|
+
.filter((candidate) => candidate.origin === "external" &&
|
|
2041
|
+
(candidate.kind === "event-subscription" ||
|
|
2042
|
+
candidate.kind === "event-reaction" ||
|
|
2043
|
+
candidate.kind === "command-reaction"))
|
|
2044
|
+
.map((candidate) => candidate.schema)));
|
|
2045
|
+
const domesticEventSchemas = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.handlers
|
|
2046
|
+
.filter((candidate) => candidate.origin === "domestic" &&
|
|
2047
|
+
(candidate.kind === "event-subscription" ||
|
|
2048
|
+
candidate.kind === "event-reaction" ||
|
|
2049
|
+
candidate.kind === "command-reaction"))
|
|
2050
|
+
.map((candidate) => candidate.schema)));
|
|
2051
|
+
const stateSchemas = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.stateSubscriptions.map((subscription) => subscription.schema)));
|
|
2052
|
+
if (entityFamily !== "projection" && stateSchemas.length > 0) {
|
|
2053
|
+
throw new Error("Entity state subscriptions are supported only by Projection repositories.");
|
|
2054
|
+
}
|
|
2055
|
+
if (stateSchemas.some((schema) => schema.typeName === metadata.schema.typeName)) {
|
|
2056
|
+
throw new Error("A Projection cannot subscribe to updates of its repository state because each " +
|
|
2057
|
+
"resulting update would be routed back to the same repository.");
|
|
2058
|
+
}
|
|
2059
|
+
const stateSubscriptions = RepositoryRoutes.stateSubscriptions(handlers);
|
|
2060
|
+
const producedEventSchemas = RepositoryHandlers.uniqueSchemas([
|
|
2061
|
+
...producedEvents,
|
|
2062
|
+
...handlers.flatMap((handler) => [
|
|
2063
|
+
...handler.commandAssignments.flatMap((assignment) => RepositoryHandlers.handlerEmittedSchemas(assignment)),
|
|
2064
|
+
...handler.eventReactions.flatMap((reaction) => RepositoryHandlers.handlerEmittedSchemas(reaction)),
|
|
2065
|
+
]),
|
|
2066
|
+
]);
|
|
2067
|
+
const producedCommandSchemas = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.commandReactions.flatMap((reaction) => RepositoryHandlers.handlerEmittedSchemas(reaction))));
|
|
2068
|
+
const commandReactions = RepositoryHandlers.createCommandReactionMap(handlers);
|
|
2069
|
+
const eventSubscribers = RepositoryHandlers.readinessMap(eventSchemas, (typeName) => eventReadiness?.findEventSubscribers(typeName) ?? []);
|
|
2070
|
+
const eventReactors = RepositoryHandlers.readinessMap(eventSchemas, (typeName) => eventReadiness?.findEventReactors(typeName) ?? []);
|
|
2071
|
+
const commandReactionFilters = RepositoryHandlers.createEventFilterPlans(commandReactions, stringifiers);
|
|
2072
|
+
const eventSubscriberFilters = RepositoryHandlers.createEventFilterPlans(eventSubscribers, stringifiers);
|
|
2073
|
+
const eventReactorFilters = RepositoryHandlers.createEventFilterPlans(eventReactors, stringifiers);
|
|
2074
|
+
const commandRoutes = RepositoryRoutes.resolveCommandRoutes(commandSchemas, commandRouting);
|
|
2075
|
+
const eventRoutes = RepositoryRoutes.resolveEventRoutes(eventSchemas, eventRouting);
|
|
2076
|
+
const stateRoutes = RepositoryRoutes.resolveStateRoutes(stateSchemas, stateUpdateRouting, metadata.idField);
|
|
2077
|
+
return Object.freeze({
|
|
2078
|
+
commandSchemas,
|
|
2079
|
+
eventSchemas,
|
|
2080
|
+
domesticEventSchemas,
|
|
2081
|
+
externalEventSchemas,
|
|
2082
|
+
producedEventSchemas,
|
|
2083
|
+
producedCommandSchemas,
|
|
2084
|
+
commandReadiness,
|
|
2085
|
+
eventReadiness,
|
|
2086
|
+
stateSchemas,
|
|
2087
|
+
stateSubscriptions,
|
|
2088
|
+
commandReactions: (eventFullTypeName, message, external) => RepositoryHandlers.forOrigin(commandReactionFilters.get(eventFullTypeName)?.select(message) ?? Object.freeze([]), external),
|
|
2089
|
+
eventReactors: (eventFullTypeName, message, external) => RepositoryHandlers.forOrigin(eventReactorFilters.get(eventFullTypeName)?.select(message) ?? Object.freeze([]), external),
|
|
2090
|
+
eventSubscribers: (eventFullTypeName, message, external) => RepositoryHandlers.forOrigin(eventSubscriberFilters.get(eventFullTypeName)?.select(message) ?? Object.freeze([]), external),
|
|
2091
|
+
routeCommand: (command) => RepositoryRoutes.routeCommand(command, commandReadiness, commandSchemas, metadata.idField, commandRoutes),
|
|
2092
|
+
routeEvent: (event) => RepositoryRoutes.routeEvent(event, eventReadiness, commandReactions, eventSchemas, metadata.idField, eventRoutes),
|
|
2093
|
+
routeStateUpdate: (event) => RepositoryRoutes.routeStateUpdate(event, stateSchemas, stateSubscriptions, metadata.idField, stateRoutes),
|
|
2094
|
+
});
|
|
2095
|
+
},
|
|
2096
|
+
routeCommand(command, readiness, schemas, targetIdField, commandRoutes) {
|
|
2097
|
+
const message = command.message;
|
|
2098
|
+
if (message === undefined || message.typeUrl === "") {
|
|
2099
|
+
throw new Error("Repository command routing requires command.message.typeUrl.");
|
|
2100
|
+
}
|
|
2101
|
+
const schema = RepositoryRoutes.schemaForTypeUrl(schemas, message.typeUrl, "command");
|
|
2102
|
+
const assignee = readiness?.findCommandAssignee(schema.typeName);
|
|
2103
|
+
if (assignee === undefined) {
|
|
2104
|
+
throw new Error(`Repository command routing has no assignee for "${schema.typeName}".`);
|
|
2105
|
+
}
|
|
2106
|
+
const customRoute = commandRoutes.get(schema);
|
|
2107
|
+
const candidateId = customRoute === undefined
|
|
2108
|
+
? RepositoryRoutes.readFirstFieldId(message, schema, "command")
|
|
2109
|
+
: RepositoryRoutes.callCommandRoute(customRoute, message, schema, command.context);
|
|
2110
|
+
return Object.freeze({
|
|
2111
|
+
entityId: RepositoryRoutes.readRouteId(candidateId, targetIdField, "command").id,
|
|
2112
|
+
messageFullTypeName: schema.typeName,
|
|
2113
|
+
invocation: "deferred",
|
|
2114
|
+
});
|
|
2115
|
+
},
|
|
2116
|
+
resolveCommandRoutes(schemas, routing) {
|
|
2117
|
+
const routes = new Map();
|
|
2118
|
+
RoutingDeclarations.validate(routing, schemas, "command");
|
|
2119
|
+
for (const schema of schemas) {
|
|
2120
|
+
const route = RoutingDeclarations.select(routing, schema);
|
|
2121
|
+
if (route !== undefined)
|
|
2122
|
+
routes.set(schema, route);
|
|
2123
|
+
}
|
|
2124
|
+
return routes;
|
|
2125
|
+
},
|
|
2126
|
+
callCommandRoute(route, message, schema, context) {
|
|
2127
|
+
const unpacked = AnyMessages.unpack(message, schema);
|
|
2128
|
+
if (unpacked === undefined) {
|
|
2129
|
+
throw new Error("Repository command routing requires a readable Command message.");
|
|
2130
|
+
}
|
|
2131
|
+
return route(unpacked, context ?? create(CommandContextSchema));
|
|
2132
|
+
},
|
|
2133
|
+
routeEvent(event, readiness, commandReactions, schemas, targetIdField, eventRoutes) {
|
|
2134
|
+
const message = event.message;
|
|
2135
|
+
if (message === undefined || message.typeUrl === "") {
|
|
2136
|
+
throw new Error("Repository event routing requires event.message.typeUrl.");
|
|
2137
|
+
}
|
|
2138
|
+
const schema = RepositoryRoutes.schemaForTypeUrl(schemas, message.typeUrl, "event");
|
|
2139
|
+
const hasReceiver = (commandReactions.get(schema.typeName)?.length ?? 0) > 0 ||
|
|
2140
|
+
(readiness?.findEventSubscribers(schema.typeName).length ?? 0) > 0 ||
|
|
2141
|
+
(readiness?.findEventReactors(schema.typeName).length ?? 0) > 0 ||
|
|
2142
|
+
(readiness?.findEventApplications(schema.typeName).length ?? 0) > 0;
|
|
2143
|
+
if (!hasReceiver) {
|
|
2144
|
+
throw new Error(`Repository event routing has no receiver for "${schema.typeName}".`);
|
|
2145
|
+
}
|
|
2146
|
+
const customRoute = eventRoutes.get(schema);
|
|
2147
|
+
if (customRoute !== undefined) {
|
|
2148
|
+
return Object.freeze({
|
|
2149
|
+
entityIds: RepositoryRoutes.callEventRoute(customRoute, message, schema, event.context, targetIdField),
|
|
2150
|
+
messageFullTypeName: schema.typeName,
|
|
2151
|
+
invocation: "deferred",
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
const targetId = RepositoryRoutes.readEventEntityId(event, message, schema, targetIdField);
|
|
2155
|
+
return Object.freeze({
|
|
2156
|
+
entityIds: Object.freeze([targetId]),
|
|
2157
|
+
messageFullTypeName: schema.typeName,
|
|
2158
|
+
invocation: "deferred",
|
|
2159
|
+
});
|
|
2160
|
+
},
|
|
2161
|
+
resolveEventRoutes(schemas, routing) {
|
|
2162
|
+
const routes = new Map();
|
|
2163
|
+
RoutingDeclarations.validate(routing, schemas, "event");
|
|
2164
|
+
for (const schema of schemas) {
|
|
2165
|
+
const route = RoutingDeclarations.select(routing, schema);
|
|
2166
|
+
if (route !== undefined)
|
|
2167
|
+
routes.set(schema, route);
|
|
2168
|
+
}
|
|
2169
|
+
return routes;
|
|
2170
|
+
},
|
|
2171
|
+
stateSubscriptions(handlers) {
|
|
2172
|
+
const byType = new Map();
|
|
2173
|
+
for (const entityHandlers of handlers) {
|
|
2174
|
+
for (const handler of entityHandlers.stateSubscriptions) {
|
|
2175
|
+
RepositoryHandlers.pushMapValue(byType, handler.messageFullTypeName, Object.freeze({
|
|
2176
|
+
entityHandlers,
|
|
2177
|
+
entityType: entityHandlers.entityType,
|
|
2178
|
+
entity: entityHandlers.entity,
|
|
2179
|
+
handler,
|
|
2180
|
+
}));
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return new Map([...byType].map(([typeName, subscriptions]) => [typeName, Object.freeze([...subscriptions])]));
|
|
2184
|
+
},
|
|
2185
|
+
resolveStateRoutes(schemas, routing, targetIdField) {
|
|
2186
|
+
const routes = new Map();
|
|
2187
|
+
RoutingDeclarations.validate(routing, schemas, "state-update");
|
|
2188
|
+
for (const schema of schemas) {
|
|
2189
|
+
const route = RoutingDeclarations.select(routing, schema);
|
|
2190
|
+
if (route !== undefined)
|
|
2191
|
+
routes.set(schema, route);
|
|
2192
|
+
else if (!schema.fields.some((field) => RepositoryRoutes.compatibleStateIdField(field, targetIdField))) {
|
|
2193
|
+
throw new Error(`Repository state-update routing has no compatible field in "${schema.typeName}".`);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return routes;
|
|
2197
|
+
},
|
|
2198
|
+
routeStateUpdate(event, schemas, subscriptions, targetIdField, routes) {
|
|
2199
|
+
const update = RepositoryRoutes.decodeStateUpdate(event, schemas, "Repository state-update routing");
|
|
2200
|
+
const candidates = subscriptions.get(update?.schema.typeName ?? "") ?? [];
|
|
2201
|
+
const interested = Object.freeze(candidates.filter((subscriber) => (subscriber.handler.origin === "external") === (event.context?.external === true)));
|
|
2202
|
+
if (update === undefined || interested.length === 0) {
|
|
2203
|
+
return undefined;
|
|
2204
|
+
}
|
|
2205
|
+
const { schema, state } = update;
|
|
2206
|
+
const custom = routes.get(schema);
|
|
2207
|
+
const candidateIds = custom === undefined
|
|
2208
|
+
? [RepositoryRoutes.firstCompatibleId(state, schema, targetIdField, "state update")]
|
|
2209
|
+
: RepositoryRoutes.callStateUpdateRoute(custom, state, event.context, targetIdField);
|
|
2210
|
+
return Object.freeze({
|
|
2211
|
+
entityIds: Object.freeze([...candidateIds]),
|
|
2212
|
+
messageFullTypeName: schema.typeName,
|
|
2213
|
+
state,
|
|
2214
|
+
subscribers: interested,
|
|
2215
|
+
invocation: "deferred",
|
|
2216
|
+
});
|
|
2217
|
+
},
|
|
2218
|
+
decodeStateUpdate(event, schemas, operation) {
|
|
2219
|
+
const message = event.message;
|
|
2220
|
+
if (message?.typeUrl !== TypeUrls.derive(EntityLog.EntityStateChangedSchema)) {
|
|
2221
|
+
throw new Error(`${operation} requires an EntityStateChanged System event.`);
|
|
2222
|
+
}
|
|
2223
|
+
const changed = AnyMessages.unpack(message, EntityLog.EntityStateChangedSchema);
|
|
2224
|
+
const packedState = changed?.newState;
|
|
2225
|
+
if (packedState === undefined || packedState.typeUrl === "") {
|
|
2226
|
+
throw new Error(`${operation} requires EntityStateChanged.newState.`);
|
|
2227
|
+
}
|
|
2228
|
+
const schema = schemas.find((candidate) => TypeUrls.derive(candidate) === packedState.typeUrl);
|
|
2229
|
+
if (schema === undefined)
|
|
2230
|
+
return undefined;
|
|
2231
|
+
const state = AnyMessages.unpack(packedState, schema);
|
|
2232
|
+
if (state === undefined)
|
|
2233
|
+
throw new Error(`${operation} requires a readable Entity state.`);
|
|
2234
|
+
return Object.freeze({ schema, state });
|
|
2235
|
+
},
|
|
2236
|
+
callStateUpdateRoute(route, state, context, targetIdField) {
|
|
2237
|
+
const candidates = route(state, context ?? create(EventContextSchema));
|
|
2238
|
+
if (!Array.isArray(candidates))
|
|
2239
|
+
throw new Error("Repository state-update routing requires an array of Entity IDs.");
|
|
2240
|
+
if (candidates.length > 1_000)
|
|
2241
|
+
throw new Error("Repository state-update routing accepts at most 1,000 Entity IDs.");
|
|
2242
|
+
const unique = new Map();
|
|
2243
|
+
for (const candidate of candidates) {
|
|
2244
|
+
const id = RepositoryRoutes.readRouteId(candidate, targetIdField, "state update").id;
|
|
2245
|
+
const key = InboxTargets.key(InboxMessages.inboxTargetId(id, targetIdField));
|
|
2246
|
+
if (!unique.has(key))
|
|
2247
|
+
unique.set(key, structuredClone(id));
|
|
2248
|
+
}
|
|
2249
|
+
return Object.freeze([...unique.values()]);
|
|
2250
|
+
},
|
|
2251
|
+
callEventRoute(route, message, schema, context, targetIdField) {
|
|
2252
|
+
const unpacked = AnyMessages.unpack(message, schema);
|
|
2253
|
+
if (unpacked === undefined) {
|
|
2254
|
+
throw new Error("Repository event routing requires a readable Event message.");
|
|
2255
|
+
}
|
|
2256
|
+
const candidates = route(unpacked, context ?? create(EventContextSchema));
|
|
2257
|
+
if (!Array.isArray(candidates)) {
|
|
2258
|
+
throw new Error("Repository event routing requires an array of Entity IDs.");
|
|
2259
|
+
}
|
|
2260
|
+
if (candidates.length > 1_000) {
|
|
2261
|
+
throw new Error("Repository event routing accepts at most 1,000 Entity IDs.");
|
|
2262
|
+
}
|
|
2263
|
+
const unique = new Map();
|
|
2264
|
+
for (const candidate of candidates) {
|
|
2265
|
+
const id = RepositoryRoutes.readRouteId(candidate, targetIdField, "event").id;
|
|
2266
|
+
const key = InboxTargets.key(InboxMessages.inboxTargetId(id, targetIdField));
|
|
2267
|
+
if (!unique.has(key))
|
|
2268
|
+
unique.set(key, structuredClone(id));
|
|
2269
|
+
}
|
|
2270
|
+
return Object.freeze([...unique.values()]);
|
|
2271
|
+
},
|
|
2272
|
+
schemaForTypeUrl(schemas, typeUrl, signalKind) {
|
|
2273
|
+
const schema = schemas.find((candidate) => TypeUrls.derive(candidate) === typeUrl);
|
|
2274
|
+
if (schema === undefined) {
|
|
2275
|
+
throw new Error(`Repository ${signalKind} routing has no schema for "${typeUrl}".`);
|
|
2276
|
+
}
|
|
2277
|
+
return schema;
|
|
2278
|
+
},
|
|
2279
|
+
readFirstFieldId(message, schema, signalKind) {
|
|
2280
|
+
const unpacked = AnyMessages.unpack(message, schema);
|
|
2281
|
+
const firstField = schema.fields[0];
|
|
2282
|
+
if (unpacked === undefined || firstField === undefined) {
|
|
2283
|
+
throw new Error(`Repository ${signalKind} routing requires a readable first field.`);
|
|
2284
|
+
}
|
|
2285
|
+
if (firstField.fieldKind === "list" || firstField.fieldKind === "map") {
|
|
2286
|
+
throw new Error(`Repository ${signalKind} routing requires a singular non-map first field.`);
|
|
2287
|
+
}
|
|
2288
|
+
const value = unpacked[firstField.localName];
|
|
2289
|
+
if (value === undefined || value === null || RepositoryRoutes.isBlankRouteId(value)) {
|
|
2290
|
+
throw new Error(`Repository ${signalKind} routing requires a non-empty first field.`);
|
|
2291
|
+
}
|
|
2292
|
+
if (signalKind === "command" && (value === 0 || value === false)) {
|
|
2293
|
+
throw new Error("Repository command routing requires a non-default first field.");
|
|
2294
|
+
}
|
|
2295
|
+
return value;
|
|
2296
|
+
},
|
|
2297
|
+
firstCompatibleId(state, schema, targetIdField, signalKind) {
|
|
2298
|
+
const field = schema.fields.find((candidate) => RepositoryRoutes.compatibleStateIdField(candidate, targetIdField));
|
|
2299
|
+
if (field === undefined)
|
|
2300
|
+
throw new Error("Repository state-update routing requires a compatible state field.");
|
|
2301
|
+
const value = state[field.localName];
|
|
2302
|
+
return RepositoryRoutes.readRouteId(value, targetIdField, signalKind).id;
|
|
2303
|
+
},
|
|
2304
|
+
compatibleStateIdField(field, targetIdField) {
|
|
2305
|
+
if (field.fieldKind === "list" || field.fieldKind === "map")
|
|
2306
|
+
return false;
|
|
2307
|
+
const target = targetIdField.descriptor;
|
|
2308
|
+
if (field.fieldKind === "message" && target.fieldKind === "message")
|
|
2309
|
+
return field.message.typeName === target.message.typeName;
|
|
2310
|
+
if (field.fieldKind !== "scalar" || target.fieldKind !== "scalar")
|
|
2311
|
+
return false;
|
|
2312
|
+
try {
|
|
2313
|
+
return (RepositoryRoutes.primitiveIdentifierType(field.scalar) ===
|
|
2314
|
+
RepositoryRoutes.primitiveIdentifierType(target.scalar));
|
|
2315
|
+
}
|
|
2316
|
+
catch {
|
|
2317
|
+
return false;
|
|
2318
|
+
}
|
|
2319
|
+
},
|
|
2320
|
+
isBlankRouteId(value) {
|
|
2321
|
+
const id = PrimitiveIds.readFinite(value) ?? MessageIds.readValue(value);
|
|
2322
|
+
return typeof id === "string" && id.trim().length === 0;
|
|
2323
|
+
},
|
|
2324
|
+
readEventEntityId(event, message, schema, targetIdField) {
|
|
2325
|
+
const producerId = event.context?.producerId;
|
|
2326
|
+
if (producerId === undefined || producerId.typeUrl.trim().length === 0) {
|
|
2327
|
+
throw new Error("Repository event routing requires a producer ID.");
|
|
2328
|
+
}
|
|
2329
|
+
const compatibleProducer = RepositoryRoutes.compatibleProducerId(producerId, targetIdField);
|
|
2330
|
+
if (compatibleProducer.compatible) {
|
|
2331
|
+
return RepositoryRoutes.readRouteId(compatibleProducer.id, targetIdField, "event").id;
|
|
2332
|
+
}
|
|
2333
|
+
return RepositoryRoutes.readRouteId(RepositoryRoutes.readFirstFieldId(message, schema, "event"), targetIdField, "event").id;
|
|
2334
|
+
},
|
|
2335
|
+
compatibleProducerId(producerId, targetIdField) {
|
|
2336
|
+
const descriptor = targetIdField.descriptor;
|
|
2337
|
+
if (descriptor.fieldKind === "message") {
|
|
2338
|
+
const schema = descriptor.message;
|
|
2339
|
+
if (producerId.typeUrl !== TypeUrls.derive(schema))
|
|
2340
|
+
return { compatible: false };
|
|
2341
|
+
const id = Identifiers.unpack(schema, producerId);
|
|
2342
|
+
if (id === undefined) {
|
|
2343
|
+
throw new Error("Repository event routing requires a readable compatible producer ID.");
|
|
2344
|
+
}
|
|
2345
|
+
return { compatible: true, id };
|
|
2346
|
+
}
|
|
2347
|
+
if (descriptor.fieldKind !== "scalar") {
|
|
2348
|
+
throw new Error("Repository event routing requires a scalar or message-valued ID.");
|
|
2349
|
+
}
|
|
2350
|
+
const type = RepositoryRoutes.primitiveIdentifierType(descriptor.scalar);
|
|
2351
|
+
const schema = type === "string"
|
|
2352
|
+
? StringValueSchema
|
|
2353
|
+
: type === "int32"
|
|
2354
|
+
? Int32ValueSchema
|
|
2355
|
+
: Int64ValueSchema;
|
|
2356
|
+
if (producerId.typeUrl !== TypeUrls.derive(schema))
|
|
2357
|
+
return { compatible: false };
|
|
2358
|
+
const id = type === "string"
|
|
2359
|
+
? Identifiers.unpack("string", producerId)
|
|
2360
|
+
: type === "int32"
|
|
2361
|
+
? Identifiers.unpack("int32", producerId)
|
|
2362
|
+
: Identifiers.unpack("int64", producerId);
|
|
2363
|
+
if (id === undefined) {
|
|
2364
|
+
throw new Error("Repository event routing requires a readable compatible producer ID.");
|
|
2365
|
+
}
|
|
2366
|
+
return { compatible: true, id };
|
|
2367
|
+
},
|
|
2368
|
+
primitiveIdentifierType(type) {
|
|
2369
|
+
switch (type) {
|
|
2370
|
+
case ScalarType.STRING:
|
|
2371
|
+
return "string";
|
|
2372
|
+
case ScalarType.INT32:
|
|
2373
|
+
case ScalarType.SINT32:
|
|
2374
|
+
case ScalarType.SFIXED32:
|
|
2375
|
+
return "int32";
|
|
2376
|
+
case ScalarType.INT64:
|
|
2377
|
+
case ScalarType.SINT64:
|
|
2378
|
+
case ScalarType.SFIXED64:
|
|
2379
|
+
return "int64";
|
|
2380
|
+
default:
|
|
2381
|
+
throw new Error("Repository event routing requires a supported Entity ID type.");
|
|
2382
|
+
}
|
|
2383
|
+
},
|
|
2384
|
+
readRouteId(value, targetIdField, signalKind) {
|
|
2385
|
+
const descriptor = targetIdField.descriptor;
|
|
2386
|
+
if (descriptor.fieldKind === "message") {
|
|
2387
|
+
return RepositoryRoutes.readMessageRouteId(value, descriptor.message.typeName, signalKind);
|
|
2388
|
+
}
|
|
2389
|
+
if (descriptor.fieldKind === "scalar") {
|
|
2390
|
+
return RepositoryRoutes.readPrimitiveRouteId(value, descriptor.scalar, signalKind);
|
|
2391
|
+
}
|
|
2392
|
+
throw new Error(`Repository ${signalKind} routing requires a scalar or message-valued ID.`);
|
|
2393
|
+
},
|
|
2394
|
+
readMessageRouteId(value, targetTypeName, signalKind) {
|
|
2395
|
+
const id = MessageIds.read(value);
|
|
2396
|
+
if (id === undefined) {
|
|
2397
|
+
throw new Error(`Repository ${signalKind} routing requires a single-field message ID.`);
|
|
2398
|
+
}
|
|
2399
|
+
if (id.$typeName !== targetTypeName) {
|
|
2400
|
+
throw new Error(`Repository ${signalKind} routing requires a "${targetTypeName}" ID.`);
|
|
2401
|
+
}
|
|
2402
|
+
return Object.freeze({
|
|
2403
|
+
id,
|
|
2404
|
+
value: id.value,
|
|
2405
|
+
});
|
|
2406
|
+
},
|
|
2407
|
+
readPrimitiveRouteId(value, targetType, signalKind) {
|
|
2408
|
+
const messageValue = MessageIds.readValue(value);
|
|
2409
|
+
const candidate = messageValue ?? value;
|
|
2410
|
+
const id = RepositoryRoutes.readCompatiblePrimitiveId(candidate, targetType);
|
|
2411
|
+
if (id === undefined) {
|
|
2412
|
+
throw new Error(`Repository ${signalKind} routing requires an ID compatible with the Entity state.`);
|
|
2413
|
+
}
|
|
2414
|
+
return Object.freeze({
|
|
2415
|
+
id,
|
|
2416
|
+
value: id,
|
|
2417
|
+
});
|
|
2418
|
+
},
|
|
2419
|
+
readCompatiblePrimitiveId(value, targetType) {
|
|
2420
|
+
switch (targetType) {
|
|
2421
|
+
case ScalarType.STRING:
|
|
2422
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2423
|
+
case ScalarType.INT32:
|
|
2424
|
+
case ScalarType.SINT32:
|
|
2425
|
+
case ScalarType.SFIXED32:
|
|
2426
|
+
return typeof value === "number" &&
|
|
2427
|
+
Number.isInteger(value) &&
|
|
2428
|
+
value >= -(2 ** 31) &&
|
|
2429
|
+
value < 2 ** 31
|
|
2430
|
+
? value
|
|
2431
|
+
: undefined;
|
|
2432
|
+
case ScalarType.INT64:
|
|
2433
|
+
case ScalarType.SINT64:
|
|
2434
|
+
case ScalarType.SFIXED64:
|
|
2435
|
+
return typeof value === "bigint" && value >= -(1n << 63n) && value < 1n << 63n
|
|
2436
|
+
? value
|
|
2437
|
+
: undefined;
|
|
2438
|
+
default:
|
|
2439
|
+
return undefined;
|
|
2440
|
+
}
|
|
2441
|
+
},
|
|
2442
|
+
};
|
|
2443
|
+
Object.freeze(RepositoryRoutes);
|
|
2444
|
+
/**
|
|
2445
|
+
* Internal repository storage operations.
|
|
2446
|
+
*/
|
|
2447
|
+
const RepositoryStorage = {
|
|
2448
|
+
openEntityStorage(factory, input) {
|
|
2449
|
+
const candidate = factory;
|
|
2450
|
+
if (candidate.createEntityStorage === undefined) {
|
|
2451
|
+
throw new Error("StorageFactory does not provide the required atomic Entity commit storage seam.");
|
|
2452
|
+
}
|
|
2453
|
+
const entity = candidate.createEntityStorage(input);
|
|
2454
|
+
const commits = EntityCommitStorageFactories.create(factory, input);
|
|
2455
|
+
return {
|
|
2456
|
+
...entity,
|
|
2457
|
+
commits,
|
|
2458
|
+
close: () => {
|
|
2459
|
+
commits.close();
|
|
2460
|
+
entity.close();
|
|
2461
|
+
},
|
|
2462
|
+
};
|
|
2463
|
+
},
|
|
2464
|
+
openRepositoryEntityStorage(repository, factory, input) {
|
|
2465
|
+
const handle = RepositoryStorage.openEntityStorage(factory, input);
|
|
2466
|
+
const key = JSON.stringify({ context: input.context, state: input.sourceType.typeName });
|
|
2467
|
+
let handles = repositoryEntityHandles.get(repository);
|
|
2468
|
+
if (handles === undefined) {
|
|
2469
|
+
handles = new Map();
|
|
2470
|
+
repositoryEntityHandles.set(repository, handles);
|
|
2471
|
+
}
|
|
2472
|
+
const existing = handles.get(key);
|
|
2473
|
+
if (existing !== undefined) {
|
|
2474
|
+
handle.close();
|
|
2475
|
+
return existing;
|
|
2476
|
+
}
|
|
2477
|
+
handles.set(key, handle);
|
|
2478
|
+
return handle;
|
|
2479
|
+
},
|
|
2480
|
+
entityStorageInput(repository, context) {
|
|
2481
|
+
const descriptor = entityStorageDescriptor(context, SpecScanner.scan(repository.entityType));
|
|
2482
|
+
const history = RepositoryStorage.historyConfiguration(repository);
|
|
2483
|
+
return {
|
|
2484
|
+
...descriptor,
|
|
2485
|
+
stateHistory: history.stateHistory,
|
|
2486
|
+
eventHistory: repository.entityFamily === "aggregate" || history.processManagerEventHistory,
|
|
2487
|
+
};
|
|
2488
|
+
},
|
|
2489
|
+
readHistoryConfiguration(options, family) {
|
|
2490
|
+
const guard = options.doubleDispatchGuard;
|
|
2491
|
+
const depth = guard === true
|
|
2492
|
+
? 100
|
|
2493
|
+
: guard === false || guard === undefined
|
|
2494
|
+
? undefined
|
|
2495
|
+
: (guard.depth ?? 100);
|
|
2496
|
+
if (depth !== undefined && (!Number.isSafeInteger(depth) || depth <= 0)) {
|
|
2497
|
+
throw new RangeError("Repository doubleDispatchGuard.depth must be a positive safe integer.");
|
|
2498
|
+
}
|
|
2499
|
+
if (depth !== undefined && family === "projection") {
|
|
2500
|
+
throw new Error("Repository doubleDispatchGuard is unavailable for Projections.");
|
|
2501
|
+
}
|
|
2502
|
+
if (depth !== undefined &&
|
|
2503
|
+
family === "process-manager" &&
|
|
2504
|
+
options.processManagerEventHistory !== true) {
|
|
2505
|
+
throw new Error("Process Manager doubleDispatchGuard requires processManagerEventHistory.");
|
|
2506
|
+
}
|
|
2507
|
+
return {
|
|
2508
|
+
stateHistory: options.stateHistory ?? false,
|
|
2509
|
+
processManagerEventHistory: options.processManagerEventHistory ?? false,
|
|
2510
|
+
dispatchGuardDepth: depth,
|
|
2511
|
+
};
|
|
2512
|
+
},
|
|
2513
|
+
historyConfiguration(repository) {
|
|
2514
|
+
const configuration = repositoryHistoryConfigurations.get(repository);
|
|
2515
|
+
if (configuration === undefined)
|
|
2516
|
+
throw new Error("Repository history configuration is unavailable.");
|
|
2517
|
+
return configuration;
|
|
2518
|
+
},
|
|
2519
|
+
/**
|
|
2520
|
+
* In-process bounded duplicate guard. Completion is recorded only after a successful execution;
|
|
2521
|
+
* therefore provider/journal failures may be retried and separate machines remain independent.
|
|
2522
|
+
*/
|
|
2523
|
+
};
|
|
2524
|
+
Object.freeze(RepositoryStorage);
|
|
2525
|
+
/**
|
|
2526
|
+
* Internal repository history internals operations.
|
|
2527
|
+
*/
|
|
2528
|
+
const RepositoryHistoryInternals = {
|
|
2529
|
+
bindEntityHistory(entity, storage, entityId, schema) {
|
|
2530
|
+
const stateCache = RepositoryHistoryInternals.createHistoryCache((depth, startingFromVersion) => storage.states.backward(entityId, depth, startingFromVersion), (record) => BigInt(EntityRecords.unpack(schema, record).versionMessage.number), { requireContiguousVersions: true });
|
|
2531
|
+
entityStateHistoryCaches.set(entity, stateCache);
|
|
2532
|
+
const eventCache = RepositoryHistoryInternals.createHistoryCache((depth, startingFromVersion) => storage.events.backward(entityId, depth, startingFromVersion), (event) => RepositorySignals.readEventVersion(event), { cacheCompleteVersionGroups: true });
|
|
2533
|
+
entityHistoryAccess.bind(entity, {
|
|
2534
|
+
stateAt: async (time) => RepositoryHistoryInternals.cloneHistoryState(await storage.states.stateAt(entityId, time)),
|
|
2535
|
+
states: async (depth) => RepositoryHistoryInternals.freezeHistoryStates((await stateCache.read(depth)).map((record) => EntityRecords.unpack(schema, record).state)),
|
|
2536
|
+
events: async (depth) => RepositoryHistoryInternals.freezeHistoryEvents(await eventCache.read(depth)),
|
|
2537
|
+
stateMaintenance: storage.states,
|
|
2538
|
+
eventMaintenance: storage.events,
|
|
2539
|
+
});
|
|
2540
|
+
},
|
|
2541
|
+
/**
|
|
2542
|
+
* Creates a per-live-entity continuation cache that serializes a larger request behind a prior read.
|
|
2543
|
+
*
|
|
2544
|
+
* @internal Shared repository history-cache implementation, exercised by repository tests.
|
|
2545
|
+
*/
|
|
2546
|
+
createHistoryCache(load, versionOf, options = {}) {
|
|
2547
|
+
const { requireContiguousVersions = false, cacheCompleteVersionGroups = false } = options;
|
|
2548
|
+
let entries = Object.freeze([]);
|
|
2549
|
+
let exhausted = false;
|
|
2550
|
+
let continuation = Promise.resolve();
|
|
2551
|
+
let nextVersion;
|
|
2552
|
+
let newestVersion;
|
|
2553
|
+
let generation = 0;
|
|
2554
|
+
const clear = () => {
|
|
2555
|
+
generation += 1;
|
|
2556
|
+
entries = Object.freeze([]);
|
|
2557
|
+
exhausted = false;
|
|
2558
|
+
nextVersion = undefined;
|
|
2559
|
+
newestVersion = undefined;
|
|
2560
|
+
};
|
|
2561
|
+
return Object.freeze({
|
|
2562
|
+
read: async (depth) => {
|
|
2563
|
+
await continuation;
|
|
2564
|
+
if (entries.length >= depth || exhausted)
|
|
2565
|
+
return entries.slice(0, depth);
|
|
2566
|
+
let result;
|
|
2567
|
+
const next = continuation.then(async () => {
|
|
2568
|
+
if (entries.length >= depth || exhausted)
|
|
2569
|
+
return;
|
|
2570
|
+
const readGeneration = generation;
|
|
2571
|
+
const requested = Math.max(1, depth - entries.length);
|
|
2572
|
+
if (cacheCompleteVersionGroups) {
|
|
2573
|
+
const loaded = await load(requested + 1, nextVersion);
|
|
2574
|
+
if (readGeneration !== generation)
|
|
2575
|
+
return;
|
|
2576
|
+
const combined = [...entries, ...loaded];
|
|
2577
|
+
const short = loaded.length < requested + 1;
|
|
2578
|
+
const terminal = loaded.at(-1);
|
|
2579
|
+
const terminalVersion = terminal === undefined ? undefined : versionOf(terminal);
|
|
2580
|
+
const cachedLength = short || terminalVersion === undefined
|
|
2581
|
+
? loaded.length
|
|
2582
|
+
: loaded.findIndex((entry) => versionOf(entry) === terminalVersion);
|
|
2583
|
+
const cacheable = loaded.slice(0, Math.max(0, cachedLength));
|
|
2584
|
+
entries = Object.freeze([...entries, ...cacheable]);
|
|
2585
|
+
const cachedLast = cacheable.at(-1);
|
|
2586
|
+
nextVersion = cachedLast === undefined ? nextVersion : versionOf(cachedLast);
|
|
2587
|
+
exhausted = short;
|
|
2588
|
+
result = Object.freeze(combined.slice(0, depth));
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
const loaded = await load(requested, nextVersion);
|
|
2592
|
+
if (readGeneration !== generation)
|
|
2593
|
+
return;
|
|
2594
|
+
const latest = loaded[0] === undefined ? undefined : versionOf(loaded[0]);
|
|
2595
|
+
const oldest = entries.at(-1);
|
|
2596
|
+
const oldestVersion = oldest === undefined ? undefined : versionOf(oldest);
|
|
2597
|
+
if (newestVersion !== undefined &&
|
|
2598
|
+
latest !== undefined &&
|
|
2599
|
+
latest > newestVersion &&
|
|
2600
|
+
nextVersion !== undefined) {
|
|
2601
|
+
clear();
|
|
2602
|
+
const refreshed = await load(depth);
|
|
2603
|
+
if (readGeneration + 1 !== generation)
|
|
2604
|
+
return;
|
|
2605
|
+
entries = Object.freeze([...refreshed]);
|
|
2606
|
+
newestVersion = refreshed[0] === undefined ? undefined : versionOf(refreshed[0]);
|
|
2607
|
+
const refreshedLast = refreshed.at(-1);
|
|
2608
|
+
nextVersion = refreshedLast === undefined ? undefined : versionOf(refreshedLast);
|
|
2609
|
+
exhausted = refreshed.length < depth;
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (requireContiguousVersions &&
|
|
2613
|
+
oldestVersion !== undefined &&
|
|
2614
|
+
latest !== undefined &&
|
|
2615
|
+
(latest >= oldestVersion || (latest < oldestVersion && latest !== oldestVersion - 1n))) {
|
|
2616
|
+
clear();
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
const combined = [...entries, ...loaded];
|
|
2620
|
+
entries = Object.freeze(combined);
|
|
2621
|
+
newestVersion ??= latest;
|
|
2622
|
+
const loadedLast = loaded.at(-1);
|
|
2623
|
+
nextVersion = loadedLast === undefined ? nextVersion : versionOf(loadedLast);
|
|
2624
|
+
exhausted = loaded.length < requested;
|
|
2625
|
+
});
|
|
2626
|
+
// spine-log-boundary: server.repository_history_prefetch
|
|
2627
|
+
continuation = next.catch(() => undefined);
|
|
2628
|
+
await next;
|
|
2629
|
+
return result ?? entries.slice(0, depth);
|
|
2630
|
+
},
|
|
2631
|
+
clear,
|
|
2632
|
+
});
|
|
2633
|
+
},
|
|
2634
|
+
cloneHistoryState(state) {
|
|
2635
|
+
return state === undefined ? undefined : Object.freeze(structuredClone(state));
|
|
2636
|
+
},
|
|
2637
|
+
freezeHistoryStates(states) {
|
|
2638
|
+
return Object.freeze(states.map((state) => Object.freeze(structuredClone(state))));
|
|
2639
|
+
},
|
|
2640
|
+
freezeHistoryEvents(events) {
|
|
2641
|
+
return Object.freeze(events.map((event) => Object.freeze(clone(EventSchema, event))));
|
|
2642
|
+
},
|
|
2643
|
+
};
|
|
2644
|
+
Object.freeze(RepositoryHistoryInternals);
|
|
2645
|
+
/**
|
|
2646
|
+
* Internal dispatch guards operations.
|
|
2647
|
+
*/
|
|
2648
|
+
const DispatchGuards = {
|
|
2649
|
+
async guardedEntityEventDispatch(repository, runtime, event, entityId, dispatch) {
|
|
2650
|
+
const depth = RepositoryStorage.historyConfiguration(repository).dispatchGuardDepth;
|
|
2651
|
+
const eventId = event.id?.value;
|
|
2652
|
+
if (depth === undefined || eventId === undefined || eventId.length === 0)
|
|
2653
|
+
return dispatch();
|
|
2654
|
+
if (entityId === undefined)
|
|
2655
|
+
return dispatch();
|
|
2656
|
+
const key = DispatchGuards.canonicalEntityIdKey(repository, entityId);
|
|
2657
|
+
const journalEventId = DispatchGuards.guardedJournalEventId(eventId, key);
|
|
2658
|
+
let guards = repositoryDispatchGuards.get(repository);
|
|
2659
|
+
if (guards === undefined) {
|
|
2660
|
+
guards = { lanes: new Map(), order: [] };
|
|
2661
|
+
repositoryDispatchGuards.set(repository, guards);
|
|
2662
|
+
}
|
|
2663
|
+
let guard = guards.lanes.get(key);
|
|
2664
|
+
if (guard === undefined) {
|
|
2665
|
+
guard = { completed: new Set(), order: [], chain: Promise.resolve(), active: 0 };
|
|
2666
|
+
guards.lanes.set(key, guard);
|
|
2667
|
+
}
|
|
2668
|
+
DispatchGuards.touchGuardLane(guards, key);
|
|
2669
|
+
const activeGuard = guard;
|
|
2670
|
+
activeGuard.active += 1;
|
|
2671
|
+
const previous = activeGuard.chain;
|
|
2672
|
+
const next = previous.then(async () => {
|
|
2673
|
+
if (activeGuard.completed.has(eventId))
|
|
2674
|
+
return;
|
|
2675
|
+
const storage = RepositoryStorage.openEntityStorage(runtime.storageFactory, RepositoryStorage.entityStorageInput(repository, RepositoryTenants.storageContextForEvent(runtime.context, event)));
|
|
2676
|
+
let persisted;
|
|
2677
|
+
try {
|
|
2678
|
+
persisted = await storage.events.backward(entityId, depth);
|
|
2679
|
+
}
|
|
2680
|
+
finally {
|
|
2681
|
+
storage.close();
|
|
2682
|
+
}
|
|
2683
|
+
if (persisted.some((candidate) => candidate.id?.value === journalEventId)) {
|
|
2684
|
+
DispatchGuards.rememberGuardCompletion(activeGuard, eventId, depth);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
await dispatch();
|
|
2688
|
+
DispatchGuards.rememberGuardCompletion(activeGuard, eventId, depth);
|
|
2689
|
+
});
|
|
2690
|
+
// spine-log-boundary: server.repository_dispatch_guard
|
|
2691
|
+
activeGuard.chain = next
|
|
2692
|
+
.catch(() => undefined)
|
|
2693
|
+
.finally(() => {
|
|
2694
|
+
activeGuard.active -= 1;
|
|
2695
|
+
DispatchGuards.trimGuardLanes(guards, depth);
|
|
2696
|
+
});
|
|
2697
|
+
return next;
|
|
2698
|
+
},
|
|
2699
|
+
rememberGuardCompletion(guard, eventId, depth) {
|
|
2700
|
+
if (!guard.completed.has(eventId)) {
|
|
2701
|
+
guard.completed.add(eventId);
|
|
2702
|
+
guard.order.push(eventId);
|
|
2703
|
+
}
|
|
2704
|
+
while (guard.order.length > depth) {
|
|
2705
|
+
const expired = guard.order.shift();
|
|
2706
|
+
if (expired !== undefined)
|
|
2707
|
+
guard.completed.delete(expired);
|
|
2708
|
+
}
|
|
2709
|
+
},
|
|
2710
|
+
touchGuardLane(guards, key) {
|
|
2711
|
+
const index = guards.order.indexOf(key);
|
|
2712
|
+
if (index >= 0)
|
|
2713
|
+
guards.order.splice(index, 1);
|
|
2714
|
+
guards.order.push(key);
|
|
2715
|
+
},
|
|
2716
|
+
trimGuardLanes(guards, depth) {
|
|
2717
|
+
while (guards.order.length > depth) {
|
|
2718
|
+
const key = guards.order[0];
|
|
2719
|
+
if (key === undefined)
|
|
2720
|
+
return;
|
|
2721
|
+
const guard = guards.lanes.get(key);
|
|
2722
|
+
if (guard?.active !== 0)
|
|
2723
|
+
return;
|
|
2724
|
+
guards.order.shift();
|
|
2725
|
+
guards.lanes.delete(key);
|
|
2726
|
+
}
|
|
2727
|
+
},
|
|
2728
|
+
guardedJournalEvent(repository, event, entityId) {
|
|
2729
|
+
const sourceId = event.id?.value;
|
|
2730
|
+
if (sourceId === undefined)
|
|
2731
|
+
return event;
|
|
2732
|
+
return create(EventSchema, {
|
|
2733
|
+
...event,
|
|
2734
|
+
id: create(EventIdSchema, {
|
|
2735
|
+
value: DispatchGuards.guardedJournalEventId(sourceId, DispatchGuards.canonicalEntityIdKey(repository, entityId)),
|
|
2736
|
+
}),
|
|
2737
|
+
});
|
|
2738
|
+
},
|
|
2739
|
+
guardedJournalEventId(sourceId, entityKey) {
|
|
2740
|
+
return `${sourceId}.guard.${encodeURIComponent(entityKey)}`;
|
|
2741
|
+
},
|
|
2742
|
+
canonicalEntityIdKey(repository, id) {
|
|
2743
|
+
return InboxTargets.key(InboxMessages.inboxTargetId(id, repository.idField));
|
|
2744
|
+
},
|
|
2745
|
+
};
|
|
2746
|
+
Object.freeze(DispatchGuards);
|
|
2747
|
+
/**
|
|
2748
|
+
* Internal inbox messages operations.
|
|
2749
|
+
*/
|
|
2750
|
+
const InboxMessages = {
|
|
2751
|
+
inboxTargetId(entityId, idField) {
|
|
2752
|
+
if (idField.descriptor.fieldKind === "message") {
|
|
2753
|
+
return Identifiers.pack(idField.descriptor.message, entityId);
|
|
2754
|
+
}
|
|
2755
|
+
if (typeof entityId === "string")
|
|
2756
|
+
return Identifiers.pack("string", entityId);
|
|
2757
|
+
if (typeof entityId === "number")
|
|
2758
|
+
return Identifiers.pack("int32", entityId);
|
|
2759
|
+
if (typeof entityId === "bigint")
|
|
2760
|
+
return Identifiers.pack("int64", entityId);
|
|
2761
|
+
throw new Error("Repository Entity Inbox handoff requires a supported target ID.");
|
|
2762
|
+
},
|
|
2763
|
+
targetEntityId(targetId, idField) {
|
|
2764
|
+
const descriptor = idField.descriptor;
|
|
2765
|
+
let entityId;
|
|
2766
|
+
if (descriptor.fieldKind === "message") {
|
|
2767
|
+
entityId = Identifiers.unpack(descriptor.message, targetId);
|
|
2768
|
+
}
|
|
2769
|
+
else if (descriptor.fieldKind === "scalar") {
|
|
2770
|
+
switch (descriptor.scalar) {
|
|
2771
|
+
case ScalarType.STRING:
|
|
2772
|
+
entityId = Identifiers.unpack("string", targetId);
|
|
2773
|
+
break;
|
|
2774
|
+
case ScalarType.INT32:
|
|
2775
|
+
case ScalarType.SINT32:
|
|
2776
|
+
case ScalarType.SFIXED32:
|
|
2777
|
+
entityId = Identifiers.unpack("int32", targetId);
|
|
2778
|
+
break;
|
|
2779
|
+
case ScalarType.INT64:
|
|
2780
|
+
case ScalarType.SINT64:
|
|
2781
|
+
case ScalarType.SFIXED64:
|
|
2782
|
+
entityId = Identifiers.unpack("int64", targetId);
|
|
2783
|
+
break;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (entityId === undefined) {
|
|
2787
|
+
throw new Error("Entity Inbox replay stored target ID is incompatible with the repository.");
|
|
2788
|
+
}
|
|
2789
|
+
return RepositoryRoutes.readRouteId(entityId, idField, "command").id;
|
|
2790
|
+
},
|
|
2791
|
+
readInboxCommand(message) {
|
|
2792
|
+
if (message.label !== "HANDLE_COMMAND") {
|
|
2793
|
+
throw new Error(`Entity Inbox replay does not handle "${message.label}" messages.`);
|
|
2794
|
+
}
|
|
2795
|
+
const command = message.signal === undefined ? undefined : AnyMessages.unpack(message.signal, CommandSchema);
|
|
2796
|
+
if (command === undefined) {
|
|
2797
|
+
throw CommandValidationError.invalidPayload();
|
|
2798
|
+
}
|
|
2799
|
+
return command;
|
|
2800
|
+
},
|
|
2801
|
+
readPmInboxEvent(message) {
|
|
2802
|
+
return InboxMessages.readStoredEvent(message, "REACT_UPON_EVENT", "Entity Inbox replay", "Entity Inbox replay requires a readable stored event.");
|
|
2803
|
+
},
|
|
2804
|
+
readProjectionInboxEvent(message) {
|
|
2805
|
+
return InboxMessages.readStoredEvent(message, "UPDATE_SUBSCRIBER", "Projection inbox replay", "Projection inbox replay requires a readable stored event.");
|
|
2806
|
+
},
|
|
2807
|
+
readStoredEvent(message, expectedLabel, replayName, unreadableMessage) {
|
|
2808
|
+
if (message.label !== expectedLabel) {
|
|
2809
|
+
throw new Error(`${replayName} does not handle "${message.label}" messages.`);
|
|
2810
|
+
}
|
|
2811
|
+
const signal = message.signal === undefined
|
|
2812
|
+
? undefined
|
|
2813
|
+
: create(AnySchema, {
|
|
2814
|
+
typeUrl: message.signal.typeUrl,
|
|
2815
|
+
value: new Uint8Array(message.signal.value),
|
|
2816
|
+
});
|
|
2817
|
+
const event = signal === undefined ? undefined : AnyMessages.unpack(signal, EventSchema);
|
|
2818
|
+
if (event === undefined) {
|
|
2819
|
+
throw new Error(unreadableMessage);
|
|
2820
|
+
}
|
|
2821
|
+
return event;
|
|
2822
|
+
},
|
|
2823
|
+
};
|
|
2824
|
+
Object.freeze(InboxMessages);
|
|
2825
|
+
/**
|
|
2826
|
+
* Internal inbox replay operations.
|
|
2827
|
+
*/
|
|
2828
|
+
const InboxReplay = {
|
|
2829
|
+
async replayAggregateCommand(repository, routing, message, deliveryTenantId) {
|
|
2830
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
2831
|
+
if (runtime === undefined) {
|
|
2832
|
+
throw new Error("Aggregate inbox replay requires a bound repository runtime.");
|
|
2833
|
+
}
|
|
2834
|
+
if (message.label !== "HANDLE_COMMAND") {
|
|
2835
|
+
throw new Error(`Aggregate inbox replay does not handle "${message.label}" messages.`);
|
|
2836
|
+
}
|
|
2837
|
+
const command = InboxMessages.readInboxCommand(message);
|
|
2838
|
+
InboxReplay.validateReplayTenant(runtime.context, deliveryTenantId, command);
|
|
2839
|
+
InboxReplay.validateReplayedCommandPayload(routing, command);
|
|
2840
|
+
const route = InboxReplay.replayCommandRoute(repository, routing, message, command);
|
|
2841
|
+
return await new AggregateCommandExecution(repository, routing, runtime, command).run(route);
|
|
2842
|
+
},
|
|
2843
|
+
async replayPmInbox(repository, routing, message, deliveryTenantId) {
|
|
2844
|
+
if (message.label === "HANDLE_COMMAND") {
|
|
2845
|
+
return await InboxReplay.replayProcessManagerCommand(repository, routing, message, deliveryTenantId);
|
|
2846
|
+
}
|
|
2847
|
+
if (message.label === "REACT_UPON_EVENT") {
|
|
2848
|
+
await InboxReplay.replayProcessManagerEvent(repository, routing, message, deliveryTenantId);
|
|
2849
|
+
return undefined;
|
|
2850
|
+
}
|
|
2851
|
+
throw new Error(`Entity Inbox replay does not handle "${message.label}" messages.`);
|
|
2852
|
+
},
|
|
2853
|
+
async replayProcessManagerCommand(repository, routing, message, deliveryTenantId) {
|
|
2854
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
2855
|
+
if (runtime === undefined) {
|
|
2856
|
+
throw new Error("Entity Inbox replay requires a bound repository runtime.");
|
|
2857
|
+
}
|
|
2858
|
+
const command = InboxMessages.readInboxCommand(message);
|
|
2859
|
+
InboxReplay.validateReplayTenant(runtime.context, deliveryTenantId, command);
|
|
2860
|
+
InboxReplay.validateReplayedCommandPayload(routing, command);
|
|
2861
|
+
const route = InboxReplay.replayCommandRoute(repository, routing, message, command);
|
|
2862
|
+
return await new ProcessManagerCommandExecution(repository, routing, runtime, command).run(route);
|
|
2863
|
+
},
|
|
2864
|
+
async replayProcessManagerEvent(repository, routing, message, deliveryTenantId) {
|
|
2865
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
2866
|
+
if (runtime === undefined) {
|
|
2867
|
+
throw new Error("Entity Inbox replay requires a bound repository runtime.");
|
|
2868
|
+
}
|
|
2869
|
+
const event = InboxMessages.readPmInboxEvent(message);
|
|
2870
|
+
InboxReplay.validatePmReplayTenant(runtime.context, deliveryTenantId, event);
|
|
2871
|
+
InboxReplay.validateReplayedEventPayload(routing, event, "Entity Inbox replay requires a readable event payload.");
|
|
2872
|
+
const route = InboxReplay.replayEventRoute(repository, routing, message, event, "Entity Inbox replay");
|
|
2873
|
+
const [entityId] = route.entityIds;
|
|
2874
|
+
await new ProcessManagerEventExecution(repository, routing, runtime, event).runTarget(entityId, route);
|
|
2875
|
+
},
|
|
2876
|
+
async replayProjectionEvent(repository, routing, message, deliveryTenantId) {
|
|
2877
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
2878
|
+
if (runtime === undefined) {
|
|
2879
|
+
throw new Error("Projection inbox replay requires a bound repository runtime.");
|
|
2880
|
+
}
|
|
2881
|
+
const event = InboxMessages.readProjectionInboxEvent(message);
|
|
2882
|
+
InboxReplay.validateProjectionReplayTenant(runtime.context, deliveryTenantId, event);
|
|
2883
|
+
InboxReplay.validateReplayedEventPayload(routing, event);
|
|
2884
|
+
const route = InboxReplay.replayEventRoute(repository, routing, message, event, "Projection inbox replay");
|
|
2885
|
+
const [entityId] = route.entityIds;
|
|
2886
|
+
await new ProjectionEventExecution(repository, routing, runtime, event).runTarget(entityId, route);
|
|
2887
|
+
},
|
|
2888
|
+
async replayProjectionMessage(repository, routing, message, deliveryTenantId) {
|
|
2889
|
+
const event = InboxMessages.readProjectionInboxEvent(message);
|
|
2890
|
+
if (event.message?.typeUrl === TypeUrls.derive(EntityLog.EntityStateChangedSchema)) {
|
|
2891
|
+
await InboxReplay.replayProjectionStateUpdate(repository, routing, message, event, deliveryTenantId);
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
await InboxReplay.replayProjectionEvent(repository, routing, message, deliveryTenantId);
|
|
2895
|
+
},
|
|
2896
|
+
async replayProjectionStateUpdate(repository, routing, message, event, deliveryTenantId) {
|
|
2897
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
2898
|
+
if (runtime === undefined)
|
|
2899
|
+
throw new Error("Projection inbox replay requires a bound repository runtime.");
|
|
2900
|
+
InboxReplay.validateProjectionReplayTenant(runtime.context, deliveryTenantId, event);
|
|
2901
|
+
const route = InboxReplay.replayStateUpdateRoute(repository, routing, message, event);
|
|
2902
|
+
const [entityId] = route.entityIds;
|
|
2903
|
+
const subscribers = route.subscribers;
|
|
2904
|
+
await ProjectionEventExecution.runStateTarget(repository, routing, runtime, event, entityId, route, subscribers);
|
|
2905
|
+
},
|
|
2906
|
+
validateReplayedCommandPayload(routing, command) {
|
|
2907
|
+
const commandMessage = EntityInvocation.requireSignalMessage(command.message, "command");
|
|
2908
|
+
const commandSchema = RepositoryRoutes.schemaForTypeUrl(routing.commandSchemas, commandMessage.typeUrl, "command");
|
|
2909
|
+
const payload = AnyMessages.unpack(commandMessage, commandSchema);
|
|
2910
|
+
if (payload === undefined) {
|
|
2911
|
+
throw CommandValidationError.invalidPayload();
|
|
2912
|
+
}
|
|
2913
|
+
try {
|
|
2914
|
+
Validate.check(commandSchema, payload);
|
|
2915
|
+
}
|
|
2916
|
+
catch (error) {
|
|
2917
|
+
if (error instanceof ValidationException) {
|
|
2918
|
+
throw new CommandValidationError(error.asMessage());
|
|
2919
|
+
}
|
|
2920
|
+
throw error;
|
|
2921
|
+
}
|
|
2922
|
+
const implicitId = ImplicitRequiredIds.validateCommand(commandSchema, payload);
|
|
2923
|
+
if (!implicitId.valid) {
|
|
2924
|
+
throw new CommandValidationError(implicitId.error);
|
|
2925
|
+
}
|
|
2926
|
+
},
|
|
2927
|
+
validateReplayedEventPayload(routing, event, invalidPayloadMessage = "Projection inbox replay requires a readable event payload.") {
|
|
2928
|
+
const eventMessage = EntityInvocation.requireSignalMessage(event.message, "event");
|
|
2929
|
+
const eventSchema = RepositoryRoutes.schemaForTypeUrl(routing.eventSchemas, eventMessage.typeUrl, "event");
|
|
2930
|
+
const payload = AnyMessages.unpack(eventMessage, eventSchema);
|
|
2931
|
+
if (payload === undefined) {
|
|
2932
|
+
throw new Error(invalidPayloadMessage);
|
|
2933
|
+
}
|
|
2934
|
+
Validate.check(eventSchema, payload);
|
|
2935
|
+
},
|
|
2936
|
+
validateReplayTenant(context, deliveryTenantId, command) {
|
|
2937
|
+
if (!context.multitenant) {
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (deliveryTenantId === undefined) {
|
|
2941
|
+
throw new Error(`Multitenant Entity Inbox replay for "${context.name}" requires tenantId.`);
|
|
2942
|
+
}
|
|
2943
|
+
const envelopeTenantId = RepositoryTenants.readCommandTenant(command);
|
|
2944
|
+
if (envelopeTenantId === undefined) {
|
|
2945
|
+
throw new Error("Entity Inbox replay requires stored command tenant metadata.");
|
|
2946
|
+
}
|
|
2947
|
+
if (!RepositoryTenants.equal(envelopeTenantId, deliveryTenantId)) {
|
|
2948
|
+
throw new Error("Entity Inbox replay stored command tenant does not match.");
|
|
2949
|
+
}
|
|
2950
|
+
},
|
|
2951
|
+
validateProjectionReplayTenant(context, deliveryTenantId, event) {
|
|
2952
|
+
if (!context.multitenant) {
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
if (deliveryTenantId === undefined) {
|
|
2956
|
+
throw new Error(`Multitenant projection inbox replay for "${context.name}" requires tenantId.`);
|
|
2957
|
+
}
|
|
2958
|
+
const envelopeTenantId = RepositoryTenants.readEventTenant(event);
|
|
2959
|
+
if (envelopeTenantId === undefined) {
|
|
2960
|
+
throw new Error("Projection inbox replay requires stored event tenant metadata.");
|
|
2961
|
+
}
|
|
2962
|
+
if (!RepositoryTenants.equal(envelopeTenantId, deliveryTenantId)) {
|
|
2963
|
+
throw new Error("Projection inbox replay stored event tenant does not match.");
|
|
2964
|
+
}
|
|
2965
|
+
},
|
|
2966
|
+
validatePmReplayTenant(context, deliveryTenantId, event) {
|
|
2967
|
+
if (!context.multitenant) {
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
if (deliveryTenantId === undefined) {
|
|
2971
|
+
throw new Error(`Multitenant Entity Inbox replay for "${context.name}" requires tenantId.`);
|
|
2972
|
+
}
|
|
2973
|
+
const envelopeTenantId = RepositoryTenants.readEventTenant(event);
|
|
2974
|
+
if (envelopeTenantId === undefined) {
|
|
2975
|
+
throw new Error("Entity Inbox replay requires stored event tenant metadata.");
|
|
2976
|
+
}
|
|
2977
|
+
if (!RepositoryTenants.equal(envelopeTenantId, deliveryTenantId)) {
|
|
2978
|
+
throw new Error("Entity Inbox replay stored event tenant does not match.");
|
|
2979
|
+
}
|
|
2980
|
+
},
|
|
2981
|
+
replayCommandRoute(repository, routing, message, command) {
|
|
2982
|
+
const expectedTargetTypeUrl = TypeUrls.derive(repository.stateSchema);
|
|
2983
|
+
if (message.inboxId.targetTypeUrl !== expectedTargetTypeUrl) {
|
|
2984
|
+
throw new Error("Entity Inbox replay stored target type does not match the routed repository.");
|
|
2985
|
+
}
|
|
2986
|
+
const commandMessage = EntityInvocation.requireSignalMessage(command.message, "command");
|
|
2987
|
+
const schema = RepositoryRoutes.schemaForTypeUrl(routing.commandSchemas, commandMessage.typeUrl, "command");
|
|
2988
|
+
return Object.freeze({
|
|
2989
|
+
entityId: InboxMessages.targetEntityId(message.inboxId.targetId, repository.idField),
|
|
2990
|
+
messageFullTypeName: schema.typeName,
|
|
2991
|
+
invocation: "deferred",
|
|
2992
|
+
});
|
|
2993
|
+
},
|
|
2994
|
+
replayEventRoute(repository, routing, message, event, replayName) {
|
|
2995
|
+
const expectedTargetTypeUrl = TypeUrls.derive(repository.stateSchema);
|
|
2996
|
+
if (message.inboxId.targetTypeUrl !== expectedTargetTypeUrl) {
|
|
2997
|
+
throw new Error(`${replayName} stored target type does not match the routed repository.`);
|
|
2998
|
+
}
|
|
2999
|
+
const eventMessage = EntityInvocation.requireSignalMessage(event.message, "event");
|
|
3000
|
+
const schema = RepositoryRoutes.schemaForTypeUrl(routing.eventSchemas, eventMessage.typeUrl, "event");
|
|
3001
|
+
const entityId = InboxMessages.targetEntityId(message.inboxId.targetId, repository.idField);
|
|
3002
|
+
const entityIds = Object.freeze([entityId]);
|
|
3003
|
+
return Object.freeze({
|
|
3004
|
+
entityIds,
|
|
3005
|
+
messageFullTypeName: schema.typeName,
|
|
3006
|
+
invocation: "deferred",
|
|
3007
|
+
});
|
|
3008
|
+
},
|
|
3009
|
+
replayStateUpdateRoute(repository, routing, message, event) {
|
|
3010
|
+
const expectedTargetTypeUrl = TypeUrls.derive(repository.stateSchema);
|
|
3011
|
+
if (message.inboxId.targetTypeUrl !== expectedTargetTypeUrl) {
|
|
3012
|
+
throw new Error("Projection inbox replay stored target type does not match the routed repository.");
|
|
3013
|
+
}
|
|
3014
|
+
const update = RepositoryRoutes.decodeStateUpdate(event, routing.stateSchemas, "Projection inbox replay");
|
|
3015
|
+
if (update === undefined ||
|
|
3016
|
+
(routing.stateSubscriptions.get(update.schema.typeName)?.length ?? 0) === 0) {
|
|
3017
|
+
throw new Error("Projection inbox replay requires a readable stored Entity state update.");
|
|
3018
|
+
}
|
|
3019
|
+
const { schema, state } = update;
|
|
3020
|
+
const entityId = InboxMessages.targetEntityId(message.inboxId.targetId, repository.idField);
|
|
3021
|
+
const entityIds = Object.freeze([entityId]);
|
|
3022
|
+
return Object.freeze({
|
|
3023
|
+
entityIds,
|
|
3024
|
+
messageFullTypeName: schema.typeName,
|
|
3025
|
+
state,
|
|
3026
|
+
subscribers: Object.freeze((() => {
|
|
3027
|
+
const candidates = routing.stateSubscriptions.get(schema.typeName) ?? [];
|
|
3028
|
+
return candidates.filter((subscriber) => (subscriber.handler.origin === "external") === (event.context?.external === true));
|
|
3029
|
+
})()),
|
|
3030
|
+
invocation: "deferred",
|
|
3031
|
+
});
|
|
3032
|
+
},
|
|
3033
|
+
};
|
|
3034
|
+
Object.freeze(InboxReplay);
|
|
3035
|
+
/**
|
|
3036
|
+
* Internal inbox handoff operations.
|
|
3037
|
+
*/
|
|
3038
|
+
const InboxHandoff = {
|
|
3039
|
+
async handoffEntityCommand(repository, runtime, command) {
|
|
3040
|
+
const route = repository.routeCommand(command);
|
|
3041
|
+
const commandId = RepositorySignals.requireCommandId(command);
|
|
3042
|
+
const whenReceived = new Date();
|
|
3043
|
+
const keepUntil = new Date(whenReceived.getTime() + inboxDedupMs);
|
|
3044
|
+
const deliveryTenantId = RepositoryTenants.requireCommandTenant(runtime.context, command);
|
|
3045
|
+
const delivery = new Delivery({
|
|
3046
|
+
context: RepositoryTenants.entityInboxDeliveryContext(runtime.context, deliveryTenantId),
|
|
3047
|
+
storageFactory: runtime.storageFactory,
|
|
3048
|
+
strategy: runtime.entityInbox.strategy(),
|
|
3049
|
+
});
|
|
3050
|
+
await runtime.entityInbox.receive(delivery, {
|
|
3051
|
+
inboxId: {
|
|
3052
|
+
targetId: InboxMessages.inboxTargetId(route.entityId, repository.idField),
|
|
3053
|
+
targetTypeUrl: TypeUrls.derive(repository.stateSchema),
|
|
3054
|
+
},
|
|
3055
|
+
signalId: commandId.uuid,
|
|
3056
|
+
signal: AnyMessages.pack(CommandSchema, command, { validate: false }),
|
|
3057
|
+
label: "HANDLE_COMMAND",
|
|
3058
|
+
status: "TO_DELIVER",
|
|
3059
|
+
keepUntil,
|
|
3060
|
+
}, deliveryTenantId);
|
|
3061
|
+
},
|
|
3062
|
+
async handoffProjectionEvent(repository, runtime, event, entityId) {
|
|
3063
|
+
const eventId = RepositorySignals.requireEventId(event);
|
|
3064
|
+
const whenReceived = new Date();
|
|
3065
|
+
const keepUntil = new Date(whenReceived.getTime() + inboxDedupMs);
|
|
3066
|
+
const deliveryTenantId = RepositoryTenants.requireProjectionTenant(runtime.context, event);
|
|
3067
|
+
const delivery = new Delivery({
|
|
3068
|
+
context: RepositoryTenants.projectionDeliveryContext(runtime.context, deliveryTenantId),
|
|
3069
|
+
storageFactory: runtime.storageFactory,
|
|
3070
|
+
});
|
|
3071
|
+
await runtime.projectionInbox.receive(delivery, {
|
|
3072
|
+
inboxId: {
|
|
3073
|
+
targetId: InboxMessages.inboxTargetId(entityId, repository.idField),
|
|
3074
|
+
targetTypeUrl: TypeUrls.derive(repository.stateSchema),
|
|
3075
|
+
},
|
|
3076
|
+
signalId: eventId.value,
|
|
3077
|
+
signal: AnyMessages.pack(EventSchema, event, { validate: false }),
|
|
3078
|
+
label: "UPDATE_SUBSCRIBER",
|
|
3079
|
+
status: "TO_DELIVER",
|
|
3080
|
+
shard: ShardIndex.single(),
|
|
3081
|
+
keepUntil,
|
|
3082
|
+
}, deliveryTenantId);
|
|
3083
|
+
},
|
|
3084
|
+
async handoffPmEvent(repository, runtime, event, entityId) {
|
|
3085
|
+
const eventId = RepositorySignals.requireEventId(event);
|
|
3086
|
+
const whenReceived = new Date();
|
|
3087
|
+
const keepUntil = new Date(whenReceived.getTime() + inboxDedupMs);
|
|
3088
|
+
const deliveryTenantId = RepositoryTenants.requirePmEventTenant(runtime.context, event);
|
|
3089
|
+
const delivery = new Delivery({
|
|
3090
|
+
context: RepositoryTenants.entityInboxDeliveryContext(runtime.context, deliveryTenantId),
|
|
3091
|
+
storageFactory: runtime.storageFactory,
|
|
3092
|
+
strategy: runtime.entityInbox.strategy(),
|
|
3093
|
+
});
|
|
3094
|
+
await runtime.entityInbox.receive(delivery, InboxHandoff.pmEventInboxInput(repository, eventId.value, event, entityId, keepUntil), deliveryTenantId);
|
|
3095
|
+
},
|
|
3096
|
+
async handoffPmEvents(repository, runtime, event, entityIds) {
|
|
3097
|
+
const eventId = RepositorySignals.requireEventId(event);
|
|
3098
|
+
const whenReceived = new Date();
|
|
3099
|
+
const keepUntil = new Date(whenReceived.getTime() + inboxDedupMs);
|
|
3100
|
+
const deliveryTenantId = RepositoryTenants.requirePmEventTenant(runtime.context, event);
|
|
3101
|
+
const delivery = new Delivery({
|
|
3102
|
+
context: RepositoryTenants.entityInboxDeliveryContext(runtime.context, deliveryTenantId),
|
|
3103
|
+
storageFactory: runtime.storageFactory,
|
|
3104
|
+
strategy: runtime.entityInbox.strategy(),
|
|
3105
|
+
});
|
|
3106
|
+
const inputs = entityIds.map((entityId) => InboxHandoff.pmEventInboxInput(repository, eventId.value, event, entityId, keepUntil));
|
|
3107
|
+
await runtime.entityInbox.receiveAll(delivery, inputs, deliveryTenantId);
|
|
3108
|
+
},
|
|
3109
|
+
pmEventInboxInput(repository, signalId, event, entityId, keepUntil) {
|
|
3110
|
+
return {
|
|
3111
|
+
inboxId: {
|
|
3112
|
+
targetId: InboxMessages.inboxTargetId(entityId, repository.idField),
|
|
3113
|
+
targetTypeUrl: TypeUrls.derive(repository.stateSchema),
|
|
3114
|
+
},
|
|
3115
|
+
signalId,
|
|
3116
|
+
signal: AnyMessages.pack(EventSchema, event, { validate: false }),
|
|
3117
|
+
label: "REACT_UPON_EVENT",
|
|
3118
|
+
status: "TO_DELIVER",
|
|
3119
|
+
keepUntil,
|
|
3120
|
+
};
|
|
3121
|
+
},
|
|
3122
|
+
};
|
|
3123
|
+
Object.freeze(InboxHandoff);
|
|
3124
|
+
/**
|
|
3125
|
+
* Internal repository dispatch operations.
|
|
3126
|
+
*/
|
|
3127
|
+
const RepositoryDispatch = {
|
|
3128
|
+
createRepositoryDispatchers(repository, routing) {
|
|
3129
|
+
const acceptedEventRoutes = new WeakMap();
|
|
3130
|
+
const acceptedStateRoutes = new WeakMap();
|
|
3131
|
+
return Object.freeze({
|
|
3132
|
+
command: routing.commandSchemas.length === 0
|
|
3133
|
+
? undefined
|
|
3134
|
+
: Object.freeze({
|
|
3135
|
+
messageSchemas: () => routing.commandSchemas,
|
|
3136
|
+
dispatch: (command) => RepositoryDispatch.dispatchRepositoryCommand(repository, routing, command),
|
|
3137
|
+
}),
|
|
3138
|
+
event: (() => {
|
|
3139
|
+
if (routing.eventSchemas.length === 0)
|
|
3140
|
+
return undefined;
|
|
3141
|
+
const dispatcher = Object.freeze({
|
|
3142
|
+
messageSchemas: () => routing.eventSchemas,
|
|
3143
|
+
externalEventSchemas: () => routing.externalEventSchemas,
|
|
3144
|
+
accept: (event) => {
|
|
3145
|
+
acceptedEventRoutes.set(event, repository.routeEvent(event));
|
|
3146
|
+
return Promise.resolve();
|
|
3147
|
+
},
|
|
3148
|
+
dispatch: (event) => {
|
|
3149
|
+
const acceptedRoute = acceptedEventRoutes.get(event);
|
|
3150
|
+
acceptedEventRoutes.delete(event);
|
|
3151
|
+
return RepositoryDispatch.dispatchRepositoryEvent(repository, routing, event, acceptedRoute);
|
|
3152
|
+
},
|
|
3153
|
+
});
|
|
3154
|
+
return EventDispatcherOriginSchemas.define(dispatcher, routing.domesticEventSchemas, routing.externalEventSchemas);
|
|
3155
|
+
})(),
|
|
3156
|
+
systemEvent: (() => {
|
|
3157
|
+
if (routing.stateSchemas.length === 0)
|
|
3158
|
+
return undefined;
|
|
3159
|
+
const hasDomestic = [...routing.stateSubscriptions.values()].some((values) => values.some((value) => value.handler.origin === "domestic"));
|
|
3160
|
+
const hasExternal = [...routing.stateSubscriptions.values()].some((values) => values.some((value) => value.handler.origin === "external"));
|
|
3161
|
+
const schema = EntityLog.EntityStateChangedSchema;
|
|
3162
|
+
const dispatcher = Object.freeze({
|
|
3163
|
+
messageSchemas: () => Object.freeze([EntityLog.EntityStateChangedSchema]),
|
|
3164
|
+
externalEventSchemas: () => (hasExternal ? Object.freeze([schema]) : Object.freeze([])),
|
|
3165
|
+
accept: (event) => {
|
|
3166
|
+
acceptedStateRoutes.set(event, routing.routeStateUpdate(event) ?? null);
|
|
3167
|
+
return Promise.resolve();
|
|
3168
|
+
},
|
|
3169
|
+
dispatch: (event) => {
|
|
3170
|
+
const route = acceptedStateRoutes.get(event);
|
|
3171
|
+
acceptedStateRoutes.delete(event);
|
|
3172
|
+
if (route === null)
|
|
3173
|
+
return Promise.resolve();
|
|
3174
|
+
return RepositoryDispatch.dispatchRepositoryStateUpdate(repository, routing, event, route);
|
|
3175
|
+
},
|
|
3176
|
+
});
|
|
3177
|
+
return EventDispatcherOriginSchemas.define(dispatcher, hasDomestic ? [schema] : [], hasExternal ? [schema] : []);
|
|
3178
|
+
})(),
|
|
3179
|
+
});
|
|
3180
|
+
},
|
|
3181
|
+
createEntityInboxTarget(repository, routing) {
|
|
3182
|
+
if ((repository.entityFamily !== "aggregate" && repository.entityFamily !== "process-manager") ||
|
|
3183
|
+
(routing.commandSchemas.length === 0 && routing.eventSchemas.length === 0)) {
|
|
3184
|
+
return undefined;
|
|
3185
|
+
}
|
|
3186
|
+
return Object.freeze({
|
|
3187
|
+
targetTypeUrl: TypeUrls.derive(repository.stateSchema),
|
|
3188
|
+
labels: Object.freeze([
|
|
3189
|
+
...(routing.commandSchemas.length === 0 ? [] : ["HANDLE_COMMAND"]),
|
|
3190
|
+
...(repository.entityFamily === "process-manager" && routing.eventSchemas.length > 0
|
|
3191
|
+
? ["REACT_UPON_EVENT"]
|
|
3192
|
+
: []),
|
|
3193
|
+
]),
|
|
3194
|
+
replay: (message, deliveryTenantId) => repository.entityFamily === "aggregate"
|
|
3195
|
+
? InboxReplay.replayAggregateCommand(repository, routing, message, deliveryTenantId)
|
|
3196
|
+
: InboxReplay.replayPmInbox(repository, routing, message, deliveryTenantId),
|
|
3197
|
+
});
|
|
3198
|
+
},
|
|
3199
|
+
createProjectionInboxTarget(repository, routing) {
|
|
3200
|
+
if (repository.entityFamily !== "projection" ||
|
|
3201
|
+
(routing.eventSchemas.length === 0 && routing.stateSchemas.length === 0)) {
|
|
3202
|
+
return undefined;
|
|
3203
|
+
}
|
|
3204
|
+
return Object.freeze({
|
|
3205
|
+
targetTypeUrl: TypeUrls.derive(repository.stateSchema),
|
|
3206
|
+
replay: (message, deliveryTenantId) => InboxReplay.replayProjectionMessage(repository, routing, message, deliveryTenantId),
|
|
3207
|
+
});
|
|
3208
|
+
},
|
|
3209
|
+
createProjectionDirectDispatch(repository, routing) {
|
|
3210
|
+
return (event, rebuild) => {
|
|
3211
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
3212
|
+
if (runtime === undefined) {
|
|
3213
|
+
void repository.routeEvent(event);
|
|
3214
|
+
return Promise.resolve();
|
|
3215
|
+
}
|
|
3216
|
+
return new ProjectionEventExecution(repository, routing, runtime, event, rebuild).runDirect();
|
|
3217
|
+
};
|
|
3218
|
+
},
|
|
3219
|
+
async dispatchRepositoryEvent(repository, routing, event, acceptedRoute) {
|
|
3220
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
3221
|
+
if (runtime === undefined) {
|
|
3222
|
+
if (acceptedRoute === undefined)
|
|
3223
|
+
void repository.routeEvent(event);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
const route = acceptedRoute ?? repository.routeEvent(event);
|
|
3227
|
+
switch (repository.entityFamily) {
|
|
3228
|
+
case "aggregate": {
|
|
3229
|
+
const execution = new AggregateEventExecution(repository, routing, runtime, event);
|
|
3230
|
+
for (const entityId of route.entityIds) {
|
|
3231
|
+
await DispatchGuards.guardedEntityEventDispatch(repository, runtime, event, entityId, () => execution.runTarget(entityId, route));
|
|
3232
|
+
}
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
case "process-manager":
|
|
3236
|
+
await new ProcessManagerEventExecution(repository, routing, runtime, event).run(route);
|
|
3237
|
+
return;
|
|
3238
|
+
case "projection":
|
|
3239
|
+
await new ProjectionEventExecution(repository, routing, runtime, event).run(route);
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
},
|
|
3243
|
+
async dispatchRepositoryStateUpdate(repository, routing, event, acceptedRoute) {
|
|
3244
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
3245
|
+
if (runtime === undefined) {
|
|
3246
|
+
if (acceptedRoute === undefined)
|
|
3247
|
+
void routing.routeStateUpdate(event);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
const route = acceptedRoute ?? routing.routeStateUpdate(event);
|
|
3251
|
+
if (route === undefined)
|
|
3252
|
+
return;
|
|
3253
|
+
for (const entityId of route.entityIds) {
|
|
3254
|
+
await InboxHandoff.handoffProjectionEvent(repository, runtime, event, entityId);
|
|
3255
|
+
}
|
|
3256
|
+
},
|
|
3257
|
+
async dispatchRepositoryCommand(repository, routing, command) {
|
|
3258
|
+
const runtime = repositoryRuntimes.get(repository);
|
|
3259
|
+
if (runtime === undefined) {
|
|
3260
|
+
void repository.routeCommand(command);
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
if (repository.entityFamily === "aggregate") {
|
|
3264
|
+
await InboxHandoff.handoffEntityCommand(repository, runtime, command);
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
if (repository.entityFamily === "process-manager") {
|
|
3268
|
+
await InboxHandoff.handoffEntityCommand(repository, runtime, command);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
void repository.routeCommand(command);
|
|
3272
|
+
},
|
|
3273
|
+
};
|
|
3274
|
+
Object.freeze(RepositoryDispatch);
|
|
3275
|
+
/**
|
|
3276
|
+
* Defines continuation-aware repository history caches.
|
|
3277
|
+
*/
|
|
3278
|
+
const repositoryHistory = {
|
|
3279
|
+
// prettier-ignore
|
|
3280
|
+
/**
|
|
3281
|
+
* Creates a cache that extends a repository history only when a caller requests more entries.
|
|
3282
|
+
*
|
|
3283
|
+
* @typeParam Entry - The history entry type.
|
|
3284
|
+
* @param load Loads entries before an optional continuation version.
|
|
3285
|
+
* @param versionOf Reads the optional version carried by an entry.
|
|
3286
|
+
* @param options Selects contiguous-version and complete-group behavior.
|
|
3287
|
+
* @returns A cache with read and clear operations.
|
|
3288
|
+
*/
|
|
3289
|
+
createCache(load, versionOf, options = {}) {
|
|
3290
|
+
return RepositoryHistoryInternals.createHistoryCache(load, versionOf, options);
|
|
3291
|
+
},
|
|
3292
|
+
};
|
|
3293
|
+
Object.freeze(repositoryHistory);
|
|
3294
|
+
/**
|
|
3295
|
+
* Defines continuation-aware repository history caches.
|
|
3296
|
+
*/
|
|
3297
|
+
export const RepositoryHistory = repositoryHistory;
|
|
3298
|
+
//# sourceMappingURL=repository.js.map
|