@spine-event-engine/server 2.0.0-snapshot.2 → 2.0.0-snapshot.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -518
- package/REFERENCE.md +498 -49
- package/dist/browser/browser-server.d.ts +95 -0
- package/dist/browser/browser-server.d.ts.map +1 -0
- package/dist/{server → browser}/browser-server.js +93 -7
- package/dist/browser/browser-server.js.map +1 -0
- package/dist/browser/durable-subscription-bindings.d.ts.map +1 -0
- package/dist/browser/durable-subscription-bindings.js.map +1 -0
- package/dist/browser/index.d.ts +12 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +24 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/options.d.ts +183 -0
- package/dist/browser/options.d.ts.map +1 -0
- package/dist/browser/options.js +15 -0
- package/dist/browser/options.js.map +1 -0
- package/dist/bus/command-bus.d.ts +2 -0
- package/dist/bus/command-bus.d.ts.map +1 -1
- package/dist/bus/command-bus.js +36 -1
- package/dist/bus/command-bus.js.map +1 -1
- package/dist/bus/event-dispatcher-registry.d.ts +2 -2
- package/dist/bus/event-dispatcher-registry.js +3 -3
- package/dist/bus/event-dispatcher-registry.js.map +1 -1
- package/dist/context/bounded-context.d.ts +238 -83
- package/dist/context/bounded-context.d.ts.map +1 -1
- package/dist/context/bounded-context.js +1083 -286
- package/dist/context/bounded-context.js.map +1 -1
- package/dist/context/effective-tenant.d.ts +9 -0
- package/dist/context/effective-tenant.d.ts.map +1 -0
- package/dist/context/effective-tenant.js +58 -0
- package/dist/context/effective-tenant.js.map +1 -0
- package/dist/context/entity-inbox.d.ts.map +1 -1
- package/dist/context/entity-inbox.js +24 -17
- package/dist/context/entity-inbox.js.map +1 -1
- package/dist/context/local-inbox-handoff.d.ts +7 -3
- package/dist/context/local-inbox-handoff.d.ts.map +1 -1
- package/dist/context/local-inbox-handoff.js +32 -26
- package/dist/context/local-inbox-handoff.js.map +1 -1
- package/dist/context/projection-handoff.d.ts.map +1 -1
- package/dist/context/projection-handoff.js +36 -31
- package/dist/context/projection-handoff.js.map +1 -1
- package/dist/context/tenant-index.d.ts.map +1 -1
- package/dist/context/tenant-index.js +72 -2
- package/dist/context/tenant-index.js.map +1 -1
- package/dist/delivery/delivery-ports.d.ts +14 -4
- package/dist/delivery/delivery-ports.d.ts.map +1 -1
- package/dist/delivery/delivery.d.ts +19 -21
- package/dist/delivery/delivery.d.ts.map +1 -1
- package/dist/delivery/delivery.js +541 -158
- package/dist/delivery/delivery.js.map +1 -1
- package/dist/delivery/inbox-storage.d.ts +12 -3
- package/dist/delivery/inbox-storage.d.ts.map +1 -1
- package/dist/delivery/inbox-storage.js +160 -67
- package/dist/delivery/inbox-storage.js.map +1 -1
- package/dist/delivery/inbox.d.ts +9 -0
- package/dist/delivery/inbox.d.ts.map +1 -1
- package/dist/delivery/inbox.js +11 -0
- package/dist/delivery/inbox.js.map +1 -1
- package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
- package/dist/delivery/sharded-work-registry.js +2 -1
- package/dist/delivery/sharded-work-registry.js.map +1 -1
- package/dist/entity/entity-metadata.d.ts +14 -5
- package/dist/entity/entity-metadata.d.ts.map +1 -1
- package/dist/entity/entity-metadata.js +38 -2
- package/dist/entity/entity-metadata.js.map +1 -1
- package/dist/entity/entity-storage-descriptor.d.ts +25 -5
- package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
- package/dist/entity/entity-storage-descriptor.js +69 -23
- package/dist/entity/entity-storage-descriptor.js.map +1 -1
- package/dist/entity/entity-transaction.d.ts +46 -42
- package/dist/entity/entity-transaction.d.ts.map +1 -1
- package/dist/entity/entity-transaction.js +130 -35
- package/dist/entity/entity-transaction.js.map +1 -1
- package/dist/entity/entity.d.ts +243 -98
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +405 -226
- package/dist/entity/entity.js.map +1 -1
- package/dist/entity/spec-scanner.d.ts +13 -1
- package/dist/entity/spec-scanner.d.ts.map +1 -1
- package/dist/entity/spec-scanner.js +20 -3
- package/dist/entity/spec-scanner.js.map +1 -1
- package/dist/handler/command-registration-readiness.d.ts +6 -6
- package/dist/handler/command-registration-readiness.d.ts.map +1 -1
- package/dist/handler/command-registration-readiness.js +4 -2
- package/dist/handler/command-registration-readiness.js.map +1 -1
- package/dist/handler/declared-rejections.d.ts +11 -0
- package/dist/handler/declared-rejections.d.ts.map +1 -0
- package/dist/handler/declared-rejections.js +34 -0
- package/dist/handler/declared-rejections.js.map +1 -0
- package/dist/handler/event-registration-readiness.d.ts +13 -56
- package/dist/handler/event-registration-readiness.d.ts.map +1 -1
- package/dist/handler/event-registration-readiness.js +23 -43
- package/dist/handler/event-registration-readiness.js.map +1 -1
- package/dist/handler/generated-handler-registry.d.ts +72 -33
- package/dist/handler/generated-handler-registry.d.ts.map +1 -1
- package/dist/handler/generated-handler-registry.js +421 -43
- package/dist/handler/generated-handler-registry.js.map +1 -1
- package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
- package/dist/handler/generated-registry-discovery.js +2 -4
- package/dist/handler/generated-registry-discovery.js.map +1 -1
- package/dist/handler/handler-decorators.d.ts +91 -37
- package/dist/handler/handler-decorators.d.ts.map +1 -1
- package/dist/handler/handler-decorators.js +124 -31
- package/dist/handler/handler-decorators.js.map +1 -1
- package/dist/handler/handler-metadata.d.ts +105 -60
- package/dist/handler/handler-metadata.d.ts.map +1 -1
- package/dist/handler/handler-metadata.js +275 -57
- package/dist/handler/handler-metadata.js.map +1 -1
- package/dist/handler/registration-readiness-metadata.d.ts +4 -0
- package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
- package/dist/handler/registration-readiness-metadata.js +84 -2
- package/dist/handler/registration-readiness-metadata.js.map +1 -1
- package/dist/handler/standalone.d.ts +25 -0
- package/dist/handler/standalone.d.ts.map +1 -0
- package/dist/handler/standalone.js +38 -0
- package/dist/handler/standalone.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/repository/command-routing.d.ts +63 -10
- package/dist/repository/command-routing.d.ts.map +1 -1
- package/dist/repository/command-routing.js +41 -8
- package/dist/repository/command-routing.js.map +1 -1
- package/dist/repository/event-routing.d.ts +64 -9
- package/dist/repository/event-routing.d.ts.map +1 -1
- package/dist/repository/event-routing.js +42 -7
- package/dist/repository/event-routing.js.map +1 -1
- package/dist/repository/primitive-id.d.ts +12 -23
- package/dist/repository/primitive-id.d.ts.map +1 -1
- package/dist/repository/primitive-id.js +2 -27
- package/dist/repository/primitive-id.js.map +1 -1
- package/dist/repository/repository.d.ts +317 -87
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +3746 -856
- package/dist/repository/repository.js.map +1 -1
- package/dist/repository/state-update-routing.d.ts +67 -8
- package/dist/repository/state-update-routing.d.ts.map +1 -1
- package/dist/repository/state-update-routing.js +45 -6
- package/dist/repository/state-update-routing.js.map +1 -1
- package/dist/runtime/runtime.js +1 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/signal-metadata.d.ts +22 -47
- package/dist/runtime/signal-metadata.d.ts.map +1 -1
- package/dist/runtime/signal-metadata.js +34 -69
- package/dist/runtime/signal-metadata.js.map +1 -1
- package/dist/runtime/signal-publisher.d.ts +144 -0
- package/dist/runtime/signal-publisher.d.ts.map +1 -0
- package/dist/runtime/signal-publisher.js +248 -0
- package/dist/runtime/signal-publisher.js.map +1 -0
- package/dist/runtime/standalone-handler-runtime.d.ts +130 -0
- package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -0
- package/dist/runtime/standalone-handler-runtime.js +382 -0
- package/dist/runtime/standalone-handler-runtime.js.map +1 -0
- package/dist/server/environment-attachment.d.ts.map +1 -1
- package/dist/server/environment-attachment.js +2 -1
- package/dist/server/environment-attachment.js.map +1 -1
- package/dist/server/environment-delivery-worker.d.ts.map +1 -1
- package/dist/server/environment-delivery-worker.js +21 -12
- package/dist/server/environment-delivery-worker.js.map +1 -1
- package/dist/server/managed-server-application.d.ts +4 -1
- package/dist/server/managed-server-application.d.ts.map +1 -1
- package/dist/server/managed-server-application.js +2 -2
- package/dist/server/managed-server-application.js.map +1 -1
- package/dist/server/node-coordinator.js +2 -2
- package/dist/server/node-coordinator.js.map +1 -1
- package/dist/server/server-log.d.ts.map +1 -1
- package/dist/server/server-log.js +4 -0
- package/dist/server/server-log.js.map +1 -1
- package/dist/server/server.d.ts +9 -177
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +428 -178
- package/dist/server/server.js.map +1 -1
- package/dist/services/query-reader.d.ts +19 -0
- package/dist/services/query-reader.d.ts.map +1 -0
- package/dist/services/query-reader.js +43 -0
- package/dist/services/query-reader.js.map +1 -0
- package/dist/services/registered-targets.d.ts +73 -0
- package/dist/services/registered-targets.d.ts.map +1 -0
- package/dist/services/registered-targets.js +127 -0
- package/dist/services/registered-targets.js.map +1 -0
- package/dist/services/spine-services.d.ts +26 -12
- package/dist/services/spine-services.d.ts.map +1 -1
- package/dist/services/spine-services.js +1047 -969
- package/dist/services/spine-services.js.map +1 -1
- package/dist/spi/delivery.d.ts +5 -0
- package/dist/spi/delivery.d.ts.map +1 -0
- package/dist/spi/delivery.js +18 -0
- package/dist/spi/delivery.js.map +1 -0
- package/dist/spi/handler-registry.d.ts +8 -0
- package/dist/spi/handler-registry.d.ts.map +1 -0
- package/dist/spi/handler-registry.js +15 -0
- package/dist/spi/handler-registry.js.map +1 -0
- package/dist/stand/stand.d.ts +73 -1
- package/dist/stand/stand.d.ts.map +1 -1
- package/dist/stand/stand.js +221 -48
- package/dist/stand/stand.js.map +1 -1
- package/dist/stand/subscription-observer.d.ts.map +1 -1
- package/dist/stand/subscription-observer.js +5 -12
- package/dist/stand/subscription-observer.js.map +1 -1
- package/dist/stand/subscription-registry.js +1 -1
- package/dist/stand/subscription-registry.js.map +1 -1
- package/dist/testing/index.d.ts +25 -3
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -23
- package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
- package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
- package/dist/handler/build-time-handler-analyzer.js +0 -1101
- package/dist/handler/build-time-handler-analyzer.js.map +0 -1
- package/dist/handler/generated-registry-writer.d.ts +0 -58
- package/dist/handler/generated-registry-writer.d.ts.map +0 -1
- package/dist/handler/generated-registry-writer.js +0 -458
- package/dist/handler/generated-registry-writer.js.map +0 -1
- package/dist/handler/handler-codegen.d.ts +0 -48
- package/dist/handler/handler-codegen.d.ts.map +0 -1
- package/dist/handler/handler-codegen.js +0 -214
- package/dist/handler/handler-codegen.js.map +0 -1
- package/dist/server/browser-server.d.ts +0 -49
- package/dist/server/browser-server.d.ts.map +0 -1
- package/dist/server/browser-server.js.map +0 -1
- package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
- package/dist/server/durable-subscription-bindings.js.map +0 -1
- /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
- /package/dist/{server → browser}/durable-subscription-bindings.js +0 -0
|
@@ -17,9 +17,10 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { clone, create, getOption, hasOption } from "@bufbuild/protobuf";
|
|
19
19
|
import { TypeUrls } from "@spine-event-engine/core";
|
|
20
|
-
import { EventSchema, BoundedContextNameSchema, TenantIdSchema, } from "@spine-event-engine/proto";
|
|
20
|
+
import { EventSchema, EventContextSchema, BoundedContextNameSchema, TenantIdSchema, } from "@spine-event-engine/proto";
|
|
21
21
|
import { SPI_type, internal_all, internal_type } from "@spine-event-engine/proto";
|
|
22
|
-
import { ColumnTypes, EventStore, InMemoryStorageFactory, RecordColumn,
|
|
22
|
+
import { ColumnTypes, EventStore, InMemoryStorageFactory, RecordColumn, } from "@spine-event-engine/storage";
|
|
23
|
+
import { TenantBoundary } from "@spine-event-engine/storage/provider";
|
|
23
24
|
import { CommandBus, commandBusAccess } from "../bus/command-bus.js";
|
|
24
25
|
import { EventBus, eventBusAccess, } from "../bus/event-bus.js";
|
|
25
26
|
import { DeliveryReadiness, } from "./local-inbox-handoff.js";
|
|
@@ -29,6 +30,7 @@ import { TenantIndexes } from "./tenant-index.js";
|
|
|
29
30
|
import { Repository, repositoryAccess, } from "../repository/repository.js";
|
|
30
31
|
import { GeneratedRegistryDiscovery } from "../handler/generated-registry-discovery.js";
|
|
31
32
|
import { HandlerRegistryIngestor, } from "../handler/generated-handler-registry.js";
|
|
33
|
+
import { AbstractAssignee, AbstractCommander, AbstractEventReactor, AbstractEventSubscriber, } from "../handler/standalone.js";
|
|
32
34
|
import { HandlerMetadataRegistry, } from "../handler/handler-metadata.js";
|
|
33
35
|
import { SignalMetadata } from "../runtime/signal-metadata.js";
|
|
34
36
|
import { Stand } from "../stand/stand.js";
|
|
@@ -37,9 +39,10 @@ import { StorageSubscriptionRegistry, } from "../stand/subscription-registry.js"
|
|
|
37
39
|
import { UniformAcrossAllShards } from "../delivery/delivery-builder.js";
|
|
38
40
|
import { InboxTargets } from "../delivery/inbox.js";
|
|
39
41
|
import { ShardIndex } from "../delivery/shard-index.js";
|
|
40
|
-
import { emitServerError } from "../server/server-log.js";
|
|
41
42
|
import { IntegrationBroker } from "../integration/integration-broker.js";
|
|
42
43
|
import { ServerEnvironment } from "../server/server-environment.js";
|
|
44
|
+
import { SignalPublisher } from "../runtime/signal-publisher.js";
|
|
45
|
+
import { StandaloneHandlerRuntime, } from "../runtime/standalone-handler-runtime.js";
|
|
43
46
|
/**
|
|
44
47
|
* Error thrown when a bounded context name cannot be accepted.
|
|
45
48
|
*/
|
|
@@ -64,10 +67,8 @@ export class BoundedContextNameError extends Error {
|
|
|
64
67
|
const frameworkConstructionToken = Object.freeze({
|
|
65
68
|
frameworkConstructionToken: true,
|
|
66
69
|
});
|
|
67
|
-
const dispatchFailureLimit = 10;
|
|
68
|
-
const dispatchErrorMessageLimit = 500;
|
|
69
|
-
const dispatchErrorStackLimit = 2_000;
|
|
70
70
|
const generatedRegistryFile = "generated/handler/generated-handler-registry.js";
|
|
71
|
+
const errorDetailLimit = 500;
|
|
71
72
|
const moduleSchemeRe = /^[A-Za-z][A-Za-z\d+.-]*:/;
|
|
72
73
|
const internalStoragePrefix = "__spine/";
|
|
73
74
|
const generatedRegistryLoadAttempts = new Map();
|
|
@@ -78,9 +79,10 @@ const contextStorageFactories = new WeakMap();
|
|
|
78
79
|
const contextDeliveryDescriptors = new WeakMap();
|
|
79
80
|
const contextSubscriptionRuntimes = new WeakMap();
|
|
80
81
|
const contextLoggers = new WeakMap();
|
|
82
|
+
const contextSignalPublishers = new WeakMap();
|
|
81
83
|
const contextEventBuses = new WeakMap();
|
|
82
84
|
const closingContexts = new WeakSet();
|
|
83
|
-
const
|
|
85
|
+
const contextClosePhases = new WeakMap();
|
|
84
86
|
const contextIntegrations = new WeakMap();
|
|
85
87
|
const systemEventPosters = new WeakMap();
|
|
86
88
|
const builderBuilds = new WeakMap();
|
|
@@ -95,13 +97,13 @@ export class BoundedContext {
|
|
|
95
97
|
#commandBus;
|
|
96
98
|
#eventBus;
|
|
97
99
|
#systemEventBus;
|
|
100
|
+
#publisher;
|
|
98
101
|
#commandEndpoint;
|
|
99
102
|
#eventEndpoint;
|
|
100
103
|
#entityInbox;
|
|
101
104
|
#projectionInbox;
|
|
102
105
|
#deliveryStrategy;
|
|
103
106
|
#registeredRepositories = [];
|
|
104
|
-
#storedEventDispatchFailures = [];
|
|
105
107
|
#repositoryViews = new Set();
|
|
106
108
|
#storageFactory;
|
|
107
109
|
#stand;
|
|
@@ -112,36 +114,25 @@ export class BoundedContext {
|
|
|
112
114
|
* Registers the framework-only construction hook for this module.
|
|
113
115
|
*/
|
|
114
116
|
static {
|
|
115
|
-
constructBoundedContext = (
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Creates a framework
|
|
119
|
-
*
|
|
120
|
-
* @param
|
|
121
|
-
* @param commandBus Dispatches commands accepted by this context.
|
|
122
|
-
* @param eventBus Dispatches events accepted by this context.
|
|
123
|
-
* @param systemEventBus Dispatches framework-only System events.
|
|
124
|
-
* @param stand Stores read-side state for this context.
|
|
125
|
-
* @param systemStand Stores read-side state for the paired System Context.
|
|
126
|
-
* @param subscriptionRuntime Coordinates pair-owned subscription delivery.
|
|
127
|
-
* @param systemSpec Contains paired System Context metadata.
|
|
128
|
-
* @param storageFactory Creates context storage.
|
|
129
|
-
* @param repositories Lists repositories to register.
|
|
130
|
-
* @param deliveryStrategy Selects immutable Entity Inbox shards.
|
|
131
|
-
* @param token Proves framework-controlled construction.
|
|
117
|
+
constructBoundedContext = (input) => new BoundedContext(input);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Creates a Bounded Context from its prepared framework services.
|
|
121
|
+
*
|
|
122
|
+
* @param input Prepared services and framework construction token.
|
|
132
123
|
*/
|
|
133
|
-
constructor(
|
|
134
|
-
ContextParts.requireFrameworkConstructionToken(token, "BoundedContext instances are framework-owned.");
|
|
135
|
-
this.#snapshot = ContextParts.cloneContextSnapshot(snapshot);
|
|
136
|
-
this.#commandBus = commandBus;
|
|
137
|
-
this.#eventBus = eventBus;
|
|
138
|
-
this.#systemEventBus = systemEventBus;
|
|
139
|
-
|
|
140
|
-
this.#stand = stand;
|
|
141
|
-
this.#systemStand = systemStand;
|
|
142
|
-
this.#subscriptionRuntime = subscriptionRuntime;
|
|
143
|
-
this.#storageFactory = storageFactory;
|
|
144
|
-
this.#deliveryStrategy = ContextParts.snapshotDeliveryStrategy(deliveryStrategy);
|
|
124
|
+
constructor(input) {
|
|
125
|
+
ContextParts.requireFrameworkConstructionToken(input.token, "BoundedContext instances are framework-owned.");
|
|
126
|
+
this.#snapshot = ContextParts.cloneContextSnapshot(input.snapshot);
|
|
127
|
+
this.#commandBus = input.commandBus;
|
|
128
|
+
this.#eventBus = input.eventBus;
|
|
129
|
+
this.#systemEventBus = input.systemEventBus;
|
|
130
|
+
this.#publisher = input.publisher;
|
|
131
|
+
this.#stand = input.stand;
|
|
132
|
+
this.#systemStand = input.systemStand;
|
|
133
|
+
this.#subscriptionRuntime = input.subscriptionRuntime;
|
|
134
|
+
this.#storageFactory = input.storageFactory;
|
|
135
|
+
this.#deliveryStrategy = ContextParts.snapshotDeliveryStrategy(input.deliveryStrategy);
|
|
145
136
|
this.#commandEndpoint = Object.freeze({
|
|
146
137
|
acceptedCommandTypes: () => this.#commandBus.acceptedCommandTypes(),
|
|
147
138
|
post: (command) => this.#commandBus.post(command),
|
|
@@ -150,9 +141,20 @@ export class BoundedContext {
|
|
|
150
141
|
acceptedEventTypes: () => ContextParts.exposedEventTypeUrls(this.#eventBus),
|
|
151
142
|
post: (event) => ContextParts.postContextEvent(this, event),
|
|
152
143
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
144
|
+
const delivery = this.#createDelivery(input.storageFactory);
|
|
145
|
+
this.#entityInbox = delivery.entityInbox;
|
|
146
|
+
this.#projectionInbox = delivery.projectionInbox;
|
|
147
|
+
this.#installReferences(input, delivery);
|
|
148
|
+
this.#registerAndStart(input.repositories, delivery.tenantIndex);
|
|
149
|
+
Object.freeze(this);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Creates tenant-aware Inboxes and their shared readiness signal.
|
|
153
|
+
*
|
|
154
|
+
* @param storageFactory Provider supplying the tenant catalog.
|
|
155
|
+
* @returns Inboxes, tenant index, and readiness signal.
|
|
156
|
+
*/
|
|
157
|
+
#createDelivery(storageFactory) {
|
|
156
158
|
const deliveryReadiness = new DeliveryReadiness();
|
|
157
159
|
const tenantIndex = TenantIndexes.create({
|
|
158
160
|
contextName: this.#snapshot.name.value,
|
|
@@ -160,15 +162,40 @@ export class BoundedContext {
|
|
|
160
162
|
storageFactory,
|
|
161
163
|
});
|
|
162
164
|
const keepTenant = (tenantId) => tenantIndex.keep(tenantId);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
const entityInbox = new LocalEntityInbox(this.#snapshot.name.value, deliveryReadiness, keepTenant, this.#deliveryStrategy);
|
|
166
|
+
const projectionInbox = new LocalProjectionInbox(this.#snapshot.name.value, deliveryReadiness, keepTenant);
|
|
167
|
+
return { deliveryReadiness, tenantIndex, entityInbox, projectionInbox };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Installs framework references used after context construction.
|
|
171
|
+
*
|
|
172
|
+
* @param input Prepared framework services.
|
|
173
|
+
* @param delivery Tenant index and Inboxes prepared for this context.
|
|
174
|
+
*/
|
|
175
|
+
#installReferences(input, delivery) {
|
|
176
|
+
contextEventBuses.set(this, [this.#eventBus, this.#systemEventBus]);
|
|
165
177
|
eventSubscribers.set(this, (typeUrl, subscriber) => eventBusAccess.subscribe(this.#eventBus, typeUrl, subscriber));
|
|
166
|
-
systemEventPosters.set(this, (event) => this.#
|
|
167
|
-
contextSystemPairings.set(this, ContextParts.createSystemPairing(this.#snapshot, systemSpec));
|
|
168
|
-
contextTenantIndexes.set(this, tenantIndex);
|
|
169
|
-
contextStorageFactories.set(this, storageFactory);
|
|
170
|
-
contextSubscriptionRuntimes.set(this, subscriptionRuntime);
|
|
171
|
-
|
|
178
|
+
systemEventPosters.set(this, (event) => this.#publisher.publishSystemEvent(event));
|
|
179
|
+
contextSystemPairings.set(this, ContextParts.createSystemPairing(this.#snapshot, input.systemSpec));
|
|
180
|
+
contextTenantIndexes.set(this, delivery.tenantIndex);
|
|
181
|
+
contextStorageFactories.set(this, input.storageFactory);
|
|
182
|
+
contextSubscriptionRuntimes.set(this, this.#subscriptionRuntime);
|
|
183
|
+
contextSignalPublishers.set(this, this.#publisher);
|
|
184
|
+
contextClosePhases.set(this, {
|
|
185
|
+
begin: () => {
|
|
186
|
+
this.#beginClose();
|
|
187
|
+
},
|
|
188
|
+
drain: () => this.#drainWork(),
|
|
189
|
+
});
|
|
190
|
+
contextDeliveryDescriptors.set(this, ContextParts.createDeliveryDescriptor(this.#snapshot, input.storageFactory, delivery.tenantIndex, this.#entityInbox, this.#projectionInbox, delivery.deliveryReadiness));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Registers repositories and starts subscriptions, closing the tenant index on failure.
|
|
194
|
+
*
|
|
195
|
+
* @param repositories Prepared repositories.
|
|
196
|
+
* @param tenantIndex Index to close if registration fails.
|
|
197
|
+
*/
|
|
198
|
+
#registerAndStart(repositories, tenantIndex) {
|
|
172
199
|
try {
|
|
173
200
|
this.#registerRepositories(repositories);
|
|
174
201
|
this.#subscriptionRuntime.start();
|
|
@@ -182,7 +209,6 @@ export class BoundedContext {
|
|
|
182
209
|
}
|
|
183
210
|
throw error;
|
|
184
211
|
}
|
|
185
|
-
Object.freeze(this);
|
|
186
212
|
}
|
|
187
213
|
#registerRepositories(repositories) {
|
|
188
214
|
const preparedRepositories = this.#prepareRepositories(repositories);
|
|
@@ -217,20 +243,13 @@ export class BoundedContext {
|
|
|
217
243
|
stand: this.#stand,
|
|
218
244
|
entityInbox: this.#entityInbox,
|
|
219
245
|
projectionInbox: this.#projectionInbox,
|
|
220
|
-
|
|
221
|
-
dispatchStoredFollowUp: (event) => eventBusAccess.postStoredFollowUp(this.#eventBus, event),
|
|
222
|
-
postEventFollowUp: (event) => eventBusAccess.postFollowUp(this.#eventBus, event),
|
|
246
|
+
publisher: this.#publisher,
|
|
223
247
|
registerEventSchema: (schema) => {
|
|
224
248
|
eventBusAccess.registerSchemas(this.#eventBus, [schema]);
|
|
225
249
|
},
|
|
226
250
|
registerSystemEventSchema: (schema) => {
|
|
227
251
|
eventBusAccess.registerSchemas(this.#systemEventBus, [schema]);
|
|
228
252
|
},
|
|
229
|
-
postSystemFollowUp: (event) => eventBusAccess.postFollowUp(this.#systemEventBus, event),
|
|
230
|
-
onPostCommand: (command) => commandBusAccess.postInternal(this.#commandBus, command),
|
|
231
|
-
recordDispatchFailure: (event, error) => {
|
|
232
|
-
this.#recordDispatchFailure(event, error);
|
|
233
|
-
},
|
|
234
253
|
};
|
|
235
254
|
const preparedRepositories = [];
|
|
236
255
|
try {
|
|
@@ -309,7 +328,7 @@ export class BoundedContext {
|
|
|
309
328
|
return ContextParts.cloneContextSnapshot(this.#snapshot);
|
|
310
329
|
}
|
|
311
330
|
/**
|
|
312
|
-
* Returns
|
|
331
|
+
* Returns this context's command endpoint.
|
|
313
332
|
*
|
|
314
333
|
* @returns Returns the context command endpoint.
|
|
315
334
|
*/
|
|
@@ -317,7 +336,7 @@ export class BoundedContext {
|
|
|
317
336
|
return this.#commandEndpoint;
|
|
318
337
|
}
|
|
319
338
|
/**
|
|
320
|
-
* Returns
|
|
339
|
+
* Returns this context's event endpoint.
|
|
321
340
|
*
|
|
322
341
|
* @returns Returns the context event endpoint.
|
|
323
342
|
*/
|
|
@@ -325,7 +344,7 @@ export class BoundedContext {
|
|
|
325
344
|
return this.#eventEndpoint;
|
|
326
345
|
}
|
|
327
346
|
/**
|
|
328
|
-
* Returns the context
|
|
347
|
+
* Returns the Stand that stores this context's read-side state.
|
|
329
348
|
*
|
|
330
349
|
* @returns Returns the read-side state store.
|
|
331
350
|
*/
|
|
@@ -340,17 +359,6 @@ export class BoundedContext {
|
|
|
340
359
|
registeredRepositories() {
|
|
341
360
|
return this.#registeredRepositories.map((snapshot) => ContextParts.createRepositoryView(snapshot));
|
|
342
361
|
}
|
|
343
|
-
/**
|
|
344
|
-
* Returns copy-safe diagnostics for asynchronous event follow-up failures.
|
|
345
|
-
*
|
|
346
|
-
* Entries can describe already-stored event dispatch or an independent
|
|
347
|
-
* follow-up post that failed before storage.
|
|
348
|
-
*
|
|
349
|
-
* @returns Returns immutable failure diagnostics.
|
|
350
|
-
*/
|
|
351
|
-
storedEventDispatchFailures() {
|
|
352
|
-
return this.#storedEventDispatchFailures.map(ContextParts.cloneDispatchFailure);
|
|
353
|
-
}
|
|
354
362
|
/**
|
|
355
363
|
* Clears and locally replays every registered Projection from already-stored events.
|
|
356
364
|
*
|
|
@@ -408,31 +416,54 @@ export class BoundedContext {
|
|
|
408
416
|
});
|
|
409
417
|
}
|
|
410
418
|
/**
|
|
411
|
-
* Closes context
|
|
419
|
+
* Closes this context's buses, Stand, and repository storage and runtime bindings.
|
|
412
420
|
*
|
|
413
421
|
* Close is idempotent and returns the same close outcome on repeated calls.
|
|
414
|
-
* The context attempts every
|
|
422
|
+
* The context attempts every close hook; when any hook fails, the
|
|
415
423
|
* returned promise rejects with an `AggregateError` after the remaining hooks
|
|
416
424
|
* have also been attempted.
|
|
417
425
|
*
|
|
418
|
-
* @returns A promise that settles after all
|
|
426
|
+
* @returns A promise that settles after all resources close.
|
|
419
427
|
*/
|
|
420
428
|
close() {
|
|
421
|
-
|
|
422
|
-
eventBusAccess.beginClose(this.#eventBus);
|
|
429
|
+
this.#beginClose();
|
|
423
430
|
this.#closed ??= this.#closeOnce();
|
|
424
431
|
return this.#closed;
|
|
425
432
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Stops admission to this context's signal and bus work.
|
|
435
|
+
*/
|
|
436
|
+
#beginClose() {
|
|
437
|
+
closingContexts.add(this);
|
|
438
|
+
this.#publisher.beginClose();
|
|
429
439
|
commandBusAccess.beginClose(this.#commandBus);
|
|
430
440
|
eventBusAccess.beginClose(this.#eventBus);
|
|
431
|
-
|
|
441
|
+
eventBusAccess.beginClose(this.#systemEventBus);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Closes integration intake and drains accepted context work before Stand close.
|
|
445
|
+
*
|
|
446
|
+
* @returns Completion after the context's accepted work settles.
|
|
447
|
+
*/
|
|
448
|
+
async #drainWork() {
|
|
449
|
+
const errors = [];
|
|
450
|
+
await ContextParts.closeContextPart(() => ContextParts.closeIntegration(this), errors);
|
|
451
|
+
await ContextParts.closeContextPart(() => ContextParts.drainContextWork(this.#commandBus, this.#eventBus, this.#systemEventBus, this.#publisher), errors);
|
|
452
|
+
if (errors.length > 0)
|
|
453
|
+
throw new AggregateError(errors, "BoundedContext drain failed.");
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Closes the context's remaining stores and repository registrations.
|
|
457
|
+
*
|
|
458
|
+
* @returns Completion after resource closure.
|
|
459
|
+
*/
|
|
460
|
+
async #closeOnce() {
|
|
461
|
+
const errors = [];
|
|
462
|
+
await ContextParts.closeContextPart(() => this.#drainWork(), errors);
|
|
463
|
+
this.#publisher.finishClose();
|
|
432
464
|
await ContextParts.closeContextPart(() => commandBusAccess.finishClose(this.#commandBus), errors);
|
|
433
465
|
await ContextParts.closeContextPart(() => eventBusAccess.finishClose(this.#eventBus), errors);
|
|
434
466
|
this.#subscriptionRuntime.beginClose();
|
|
435
|
-
eventBusAccess.beginClose(this.#systemEventBus);
|
|
436
467
|
await ContextParts.closeContextPart(() => eventBusAccess.drain(this.#systemEventBus), errors);
|
|
437
468
|
await ContextParts.closeContextPart(() => this.#subscriptionRuntime.drainClose(), errors);
|
|
438
469
|
await ContextParts.closeContextPart(() => eventBusAccess.finishClose(this.#systemEventBus), errors);
|
|
@@ -442,34 +473,24 @@ export class BoundedContext {
|
|
|
442
473
|
await ContextParts.closeContextPart(() => {
|
|
443
474
|
ContextParts.requireTenantIndex(this).close();
|
|
444
475
|
}, errors);
|
|
445
|
-
|
|
446
|
-
await ContextParts.closeContextPart(() => {
|
|
447
|
-
repositoryAccess.clearRuntime(repository);
|
|
448
|
-
registeredRepositories.delete(repository);
|
|
449
|
-
}, errors);
|
|
450
|
-
}
|
|
476
|
+
await this.#closeRepositories(errors);
|
|
451
477
|
if (errors.length > 0) {
|
|
452
478
|
this.#closed = undefined;
|
|
453
479
|
throw new AggregateError(ContextParts.flattenErrors(errors), "BoundedContext close failed.");
|
|
454
480
|
}
|
|
455
481
|
ContextParts.clearContextMetadata(this);
|
|
456
482
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
emitServerError(logger, "Repository follow-up dispatch failed.", {
|
|
469
|
-
eventType,
|
|
470
|
-
operation: "repository.follow_up",
|
|
471
|
-
reasonCode: "dispatch_failed",
|
|
472
|
-
});
|
|
483
|
+
/**
|
|
484
|
+
* Clears repository runtime registration after Stand closure.
|
|
485
|
+
*
|
|
486
|
+
* @param errors Failures collected during context closure.
|
|
487
|
+
*/
|
|
488
|
+
async #closeRepositories(errors) {
|
|
489
|
+
for (const repository of this.#repositoryViews) {
|
|
490
|
+
await ContextParts.closeContextPart(() => {
|
|
491
|
+
repositoryAccess.clearRuntime(repository);
|
|
492
|
+
registeredRepositories.delete(repository);
|
|
493
|
+
}, errors);
|
|
473
494
|
}
|
|
474
495
|
}
|
|
475
496
|
}
|
|
@@ -477,6 +498,18 @@ export class BoundedContext {
|
|
|
477
498
|
* Exposes framework-only operations for built contexts and their builders.
|
|
478
499
|
*/
|
|
479
500
|
export const boundedContextAccess = Object.freeze({
|
|
501
|
+
beginClose(context) {
|
|
502
|
+
const phase = contextClosePhases.get(context);
|
|
503
|
+
if (phase === undefined)
|
|
504
|
+
throw new TypeError("Close phase requires a built BoundedContext.");
|
|
505
|
+
phase.begin();
|
|
506
|
+
},
|
|
507
|
+
drainWork(context) {
|
|
508
|
+
const phase = contextClosePhases.get(context);
|
|
509
|
+
if (phase === undefined)
|
|
510
|
+
return Promise.reject(new TypeError("Drain requires a built BoundedContext."));
|
|
511
|
+
return phase.drain();
|
|
512
|
+
},
|
|
480
513
|
installLogger(context, logger) {
|
|
481
514
|
if (!contextStorageFactories.has(context)) {
|
|
482
515
|
throw new TypeError("Context logger requires a built BoundedContext instance.");
|
|
@@ -488,6 +521,10 @@ export const boundedContextAccess = Object.freeze({
|
|
|
488
521
|
}
|
|
489
522
|
eventBusAccess.installLogger(buses[0], logger);
|
|
490
523
|
eventBusAccess.installLogger(buses[1], logger);
|
|
524
|
+
const publisher = contextSignalPublishers.get(context);
|
|
525
|
+
if (publisher === undefined)
|
|
526
|
+
throw new TypeError("Context logger requires a built BoundedContext instance.");
|
|
527
|
+
publisher.installLogger(logger);
|
|
491
528
|
const runtime = contextSubscriptionRuntimes.get(context);
|
|
492
529
|
if (runtime === undefined) {
|
|
493
530
|
throw new TypeError("Context logger requires a built BoundedContext instance.");
|
|
@@ -501,13 +538,6 @@ export const boundedContextAccess = Object.freeze({
|
|
|
501
538
|
}
|
|
502
539
|
return logger;
|
|
503
540
|
},
|
|
504
|
-
recordDispatchFailure(context, event, error) {
|
|
505
|
-
const record = contextDispatchFailureRecorders.get(context);
|
|
506
|
-
if (record === undefined) {
|
|
507
|
-
throw new TypeError("Dispatch failure recording requires a built BoundedContext instance.");
|
|
508
|
-
}
|
|
509
|
-
record(event, error);
|
|
510
|
-
},
|
|
511
541
|
isBuilder(value) {
|
|
512
542
|
return (typeof value === "object" &&
|
|
513
543
|
value !== null &&
|
|
@@ -534,6 +564,19 @@ export const boundedContextAccess = Object.freeze({
|
|
|
534
564
|
}
|
|
535
565
|
return post(event);
|
|
536
566
|
},
|
|
567
|
+
postExternalEvent(context, event) {
|
|
568
|
+
const imported = clone(EventSchema, event);
|
|
569
|
+
imported.context = clone(EventContextSchema, imported.context ?? create(EventContextSchema));
|
|
570
|
+
imported.context.external = true;
|
|
571
|
+
return context.eventBus().post(imported);
|
|
572
|
+
},
|
|
573
|
+
observeProducedSignals(context, observer) {
|
|
574
|
+
const publisher = contextSignalPublishers.get(context);
|
|
575
|
+
if (publisher === undefined) {
|
|
576
|
+
throw new TypeError("Produced signal observation requires a built BoundedContext instance.");
|
|
577
|
+
}
|
|
578
|
+
return publisher.observe(observer);
|
|
579
|
+
},
|
|
537
580
|
systemPairing(context) {
|
|
538
581
|
return ContextParts.cloneSystemPairing(ContextParts.requireSystemPairing(context));
|
|
539
582
|
},
|
|
@@ -576,6 +619,9 @@ export class BoundedContextBuilder {
|
|
|
576
619
|
#specSnapshot;
|
|
577
620
|
#commandDispatchers = new Set();
|
|
578
621
|
#eventDispatchers = new Set();
|
|
622
|
+
#assignees = [];
|
|
623
|
+
#commanders = [];
|
|
624
|
+
#eventReceivers = [];
|
|
579
625
|
#repositories = new Set();
|
|
580
626
|
#entityTypes = new Set();
|
|
581
627
|
#generatedRepositoryOptions = new Map();
|
|
@@ -591,7 +637,7 @@ export class BoundedContextBuilder {
|
|
|
591
637
|
constructBoundedContextBuilder = (snapshot, token) => new BoundedContextBuilder(snapshot, token);
|
|
592
638
|
}
|
|
593
639
|
/**
|
|
594
|
-
* Creates a
|
|
640
|
+
* Creates a builder from a prepared context specification.
|
|
595
641
|
*
|
|
596
642
|
* @param specSnapshot Contains the initial context specification.
|
|
597
643
|
* @param token Proves framework-controlled construction.
|
|
@@ -637,6 +683,7 @@ export class BoundedContextBuilder {
|
|
|
637
683
|
/**
|
|
638
684
|
* Adds an explicitly assembled repository or an Entity class.
|
|
639
685
|
*
|
|
686
|
+
* @typeParam EntityType Concrete Entity class represented by this entry.
|
|
640
687
|
* @param entry The repository or Entity class to register.
|
|
641
688
|
* @param options Optional settings used only when an Entity class is supplied.
|
|
642
689
|
* @returns This builder for further configuration.
|
|
@@ -659,6 +706,7 @@ export class BoundedContextBuilder {
|
|
|
659
706
|
/**
|
|
660
707
|
* Removes a repository from the context registration list.
|
|
661
708
|
*
|
|
709
|
+
* @typeParam EntityType Concrete Entity class represented by the repository.
|
|
662
710
|
* @param repository Identifies the repository to remove.
|
|
663
711
|
* @returns Returns this builder for further configuration.
|
|
664
712
|
*/
|
|
@@ -670,10 +718,17 @@ export class BoundedContextBuilder {
|
|
|
670
718
|
/**
|
|
671
719
|
* Adds a command dispatcher to the context being built.
|
|
672
720
|
*
|
|
673
|
-
* @param dispatcher
|
|
674
|
-
*
|
|
721
|
+
* @param dispatcher Raw command dispatcher, or an `AbstractCommander` instance.
|
|
722
|
+
* A standalone commander requires generated receiver metadata and therefore
|
|
723
|
+
* this builder's `buildAsync()` path. It installs its Command and Event sides
|
|
724
|
+
* exactly once.
|
|
725
|
+
* @returns This builder for further configuration.
|
|
675
726
|
*/
|
|
676
727
|
addCommandDispatcher(dispatcher) {
|
|
728
|
+
if (dispatcher instanceof AbstractCommander) {
|
|
729
|
+
this.#commanders.push(dispatcher);
|
|
730
|
+
return this;
|
|
731
|
+
}
|
|
677
732
|
this.#commandDispatchers.add(dispatcher);
|
|
678
733
|
return this;
|
|
679
734
|
}
|
|
@@ -690,13 +745,32 @@ export class BoundedContextBuilder {
|
|
|
690
745
|
/**
|
|
691
746
|
* Adds an event dispatcher to the context being built.
|
|
692
747
|
*
|
|
693
|
-
* @param dispatcher
|
|
694
|
-
*
|
|
748
|
+
* @param dispatcher Raw event dispatcher, or a standalone reactor/subscriber.
|
|
749
|
+
* Standalone receivers require generated receiver metadata and `buildAsync()`.
|
|
750
|
+
* @returns This builder for further configuration.
|
|
695
751
|
*/
|
|
696
752
|
addEventDispatcher(dispatcher) {
|
|
753
|
+
if (dispatcher instanceof AbstractEventReactor ||
|
|
754
|
+
dispatcher instanceof AbstractEventSubscriber) {
|
|
755
|
+
this.#eventReceivers.push(dispatcher);
|
|
756
|
+
return this;
|
|
757
|
+
}
|
|
697
758
|
this.#eventDispatchers.add(dispatcher);
|
|
698
759
|
return this;
|
|
699
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Adds a generated standalone command assignee.
|
|
763
|
+
*
|
|
764
|
+
* The registered instance is matched by exact constructor to generated
|
|
765
|
+
* receiver metadata when `buildAsync()` assembles the context.
|
|
766
|
+
*
|
|
767
|
+
* @param assignee Generated standalone assignee instance.
|
|
768
|
+
* @returns This builder for further configuration.
|
|
769
|
+
*/
|
|
770
|
+
addAssignee(assignee) {
|
|
771
|
+
this.#assignees.push(assignee);
|
|
772
|
+
return this;
|
|
773
|
+
}
|
|
700
774
|
/**
|
|
701
775
|
* Removes an event dispatcher from the context being built.
|
|
702
776
|
*
|
|
@@ -743,7 +817,7 @@ export class BoundedContextBuilder {
|
|
|
743
817
|
return this;
|
|
744
818
|
}
|
|
745
819
|
/**
|
|
746
|
-
* Sets
|
|
820
|
+
* Sets how Entity Inbox targets are assigned to shards.
|
|
747
821
|
*
|
|
748
822
|
* @param strategy Selects the durable shard for Aggregate and Process Manager targets.
|
|
749
823
|
* @returns Returns this builder for further configuration.
|
|
@@ -768,11 +842,19 @@ export class BoundedContextBuilder {
|
|
|
768
842
|
* @returns Returns the built context.
|
|
769
843
|
*/
|
|
770
844
|
build() {
|
|
845
|
+
if (this.#standaloneInstances().length > 0) {
|
|
846
|
+
throw new Error("Standalone generated handlers require buildAsync().");
|
|
847
|
+
}
|
|
771
848
|
ContextParts.rejectSyncEntityAssembly(this.#entityTypes);
|
|
772
849
|
return this.#buildWith([...this.#repositories], this.#storageFactory ?? new InMemoryStorageFactory());
|
|
773
850
|
}
|
|
774
851
|
/**
|
|
775
|
-
* Builds a context after loading generated metadata for added entity classes
|
|
852
|
+
* Builds a context after loading generated metadata for added entity classes
|
|
853
|
+
* and registered standalone handlers.
|
|
854
|
+
*
|
|
855
|
+
* Standalone assignees, commanders, reactors, and subscribers require this
|
|
856
|
+
* asynchronous path so their exact constructors can be matched to generated
|
|
857
|
+
* receiver metadata.
|
|
776
858
|
*
|
|
777
859
|
* @returns Resolves to the built context.
|
|
778
860
|
*/
|
|
@@ -780,11 +862,9 @@ export class BoundedContextBuilder {
|
|
|
780
862
|
return this.#buildAsyncWith();
|
|
781
863
|
}
|
|
782
864
|
async #buildAsyncWith(defaultStorageFactory) {
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
];
|
|
787
|
-
const context = this.#buildWith(repositories, this.#storageFactory ?? defaultStorageFactory ?? new InMemoryStorageFactory());
|
|
865
|
+
const generated = await this.#loadGeneratedArtifacts([...this.#entityTypes]);
|
|
866
|
+
const repositories = [...this.#repositories, ...generated.repositories];
|
|
867
|
+
const context = this.#buildWith(repositories, this.#storageFactory ?? defaultStorageFactory ?? new InMemoryStorageFactory(), generated.standalone);
|
|
788
868
|
try {
|
|
789
869
|
await ContextParts.integrationReady(context);
|
|
790
870
|
}
|
|
@@ -799,88 +879,146 @@ export class BoundedContextBuilder {
|
|
|
799
879
|
}
|
|
800
880
|
return context;
|
|
801
881
|
}
|
|
802
|
-
#buildWith(repositories, storageFactory) {
|
|
803
|
-
|
|
882
|
+
#buildWith(repositories, storageFactory, standalone = []) {
|
|
883
|
+
const resources = this.#subscriptionRegistry === undefined ? {} : { registry: this.#subscriptionRegistry };
|
|
804
884
|
this.#subscriptionRegistry = undefined;
|
|
805
885
|
const registeredRepositories = [...repositories];
|
|
806
|
-
let eventStore;
|
|
807
|
-
let systemEventStore;
|
|
808
|
-
let eventBus;
|
|
809
|
-
let systemEventBus;
|
|
810
|
-
let stand;
|
|
811
|
-
let systemStand;
|
|
812
|
-
let runtime;
|
|
813
886
|
try {
|
|
814
|
-
|
|
815
|
-
const eventDispatchers = [
|
|
816
|
-
...ContextParts.repositoryEventDispatchers(registeredRepositories),
|
|
817
|
-
...this.#eventDispatchers,
|
|
818
|
-
];
|
|
819
|
-
const repositorySystemEventDispatchers = ContextParts.repositorySystemEventDispatchers(registeredRepositories);
|
|
820
|
-
const domainEventDispatchers = ContextParts.domainEventDispatchers(eventDispatchers);
|
|
821
|
-
const systemEventDispatchers = [
|
|
822
|
-
...ContextParts.systemEventDispatchers(eventDispatchers),
|
|
823
|
-
...repositorySystemEventDispatchers,
|
|
824
|
-
];
|
|
825
|
-
const commandBus = new CommandBus([
|
|
826
|
-
...this.#commandDispatchers,
|
|
827
|
-
...ContextParts.repositoryCommandDispatchers(registeredRepositories),
|
|
828
|
-
]);
|
|
829
|
-
const systemSpec = ContextParts.createSystemSpec(this.#specSnapshot, this.#persistSystemEvents);
|
|
830
|
-
systemEventStore = systemSpec.storesEvents
|
|
831
|
-
? new EventStore(ContextParts.createStorageMode(systemSpec), storageFactory)
|
|
832
|
-
: undefined;
|
|
833
|
-
systemEventBus = eventBusAccess.createSystemBus(systemEventStore);
|
|
834
|
-
for (const dispatcher of systemEventDispatchers)
|
|
835
|
-
systemEventBus.register(dispatcher);
|
|
836
|
-
systemStand = new Stand({
|
|
837
|
-
context: ContextParts.createStorageMode(systemSpec),
|
|
838
|
-
storageFactory,
|
|
839
|
-
});
|
|
840
|
-
eventStore = this.createEventStore(storageFactory);
|
|
841
|
-
eventBus = new EventBus(eventStore);
|
|
842
|
-
for (const dispatcher of domainEventDispatchers)
|
|
843
|
-
eventBus.register(dispatcher);
|
|
844
|
-
eventBusAccess.registerSchemas(eventBus, ContextParts.repositoryProducedEventSchemas(registeredRepositories));
|
|
845
|
-
stand = new Stand({
|
|
846
|
-
context: ContextParts.createStorageMode(this.#specSnapshot),
|
|
847
|
-
storageFactory,
|
|
848
|
-
});
|
|
849
|
-
registry ??= new StorageSubscriptionRegistry(ContextParts.createSubscriptionStorageContext(this.#specSnapshot), storageFactory);
|
|
850
|
-
runtime = new SubscriptionRuntime(stand, systemStand, eventBus, systemEventBus, registry);
|
|
851
|
-
const context = ContextParts.createBoundedContext(this.#specSnapshot, commandBus, eventBus, systemEventBus, stand, systemStand, runtime, systemSpec, storageFactory, registeredRepositories, this.#deliveryStrategy);
|
|
852
|
-
ContextParts.attachIntegration(context, eventBus, systemSpec, ContextParts.externalEventSchemas(domainEventDispatchers));
|
|
853
|
-
return context;
|
|
887
|
+
return this.#assembleContext(registeredRepositories, storageFactory, standalone, resources);
|
|
854
888
|
}
|
|
855
889
|
catch (error) {
|
|
856
|
-
|
|
857
|
-
ContextParts.attemptCleanup(() => runtime?.abortClose(), cleanupErrors);
|
|
858
|
-
if (runtime === undefined) {
|
|
859
|
-
ContextParts.attemptCleanup(() => void registry?.close().catch(() => undefined), cleanupErrors);
|
|
860
|
-
}
|
|
861
|
-
ContextParts.attemptCleanup(() => void stand?.close().catch(() => undefined), cleanupErrors);
|
|
862
|
-
ContextParts.attemptCleanup(() => void systemStand?.close().catch(() => undefined), cleanupErrors);
|
|
863
|
-
ContextParts.attemptCleanup(() => {
|
|
864
|
-
if (systemEventBus !== undefined)
|
|
865
|
-
eventBusAccess.abortClose(systemEventBus);
|
|
866
|
-
else if (systemEventStore !== undefined)
|
|
867
|
-
systemEventStore.close();
|
|
868
|
-
}, cleanupErrors);
|
|
869
|
-
ContextParts.attemptCleanup(() => {
|
|
870
|
-
if (eventBus !== undefined)
|
|
871
|
-
eventBusAccess.abortClose(eventBus);
|
|
872
|
-
else if (eventStore !== undefined)
|
|
873
|
-
eventStore.close();
|
|
874
|
-
}, cleanupErrors);
|
|
875
|
-
if (cleanupErrors.length > 0) {
|
|
876
|
-
throw new AggregateError([error, ...cleanupErrors], "Bounded Context build failed during cleanup.");
|
|
877
|
-
}
|
|
878
|
-
throw error;
|
|
890
|
+
return this.#failBuild(resources, error);
|
|
879
891
|
}
|
|
880
892
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
893
|
+
#assembleContext(repositories, storageFactory, standalone, resources) {
|
|
894
|
+
ContextParts.preflightRepositories(repositories);
|
|
895
|
+
const dispatchers = this.#buildDispatchers(repositories);
|
|
896
|
+
const buses = this.#buildBuses(repositories, storageFactory, dispatchers, resources);
|
|
897
|
+
const standaloneEvent = this.#installStandalone(standalone, buses);
|
|
898
|
+
for (const dispatcher of dispatchers.domain)
|
|
899
|
+
buses.eventBus.register(dispatcher);
|
|
900
|
+
eventBusAccess.registerSchemas(buses.eventBus, [
|
|
901
|
+
...ContextParts.repositoryProducedEventSchemas(repositories),
|
|
902
|
+
...ContextParts.standaloneProducedEventSchemas(standalone),
|
|
903
|
+
]);
|
|
904
|
+
const running = this.#buildRuntime(storageFactory, buses, resources);
|
|
905
|
+
const context = this.#createContext(repositories, storageFactory, buses, running);
|
|
906
|
+
ContextParts.attachIntegration(context, buses.eventBus, buses.systemSpec, ContextParts.externalEventSchemas([
|
|
907
|
+
...dispatchers.domain,
|
|
908
|
+
...(standaloneEvent === undefined ? [] : [standaloneEvent]),
|
|
909
|
+
]));
|
|
910
|
+
return context;
|
|
911
|
+
}
|
|
912
|
+
#buildDispatchers(repositories) {
|
|
913
|
+
const event = [
|
|
914
|
+
...ContextParts.repositoryEventDispatchers(repositories),
|
|
915
|
+
...this.#eventDispatchers,
|
|
916
|
+
];
|
|
917
|
+
return {
|
|
918
|
+
domain: ContextParts.domainEventDispatchers(event),
|
|
919
|
+
system: [
|
|
920
|
+
...ContextParts.systemEventDispatchers(event),
|
|
921
|
+
...ContextParts.repositorySystemEventDispatchers(repositories),
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
#buildBuses(repositories, storageFactory, dispatchers, resources) {
|
|
926
|
+
const commandBus = new CommandBus([
|
|
927
|
+
...this.#commandDispatchers,
|
|
928
|
+
...ContextParts.repositoryCommandDispatchers(repositories),
|
|
929
|
+
]);
|
|
930
|
+
resources.commandBus = commandBus;
|
|
931
|
+
const system = this.#buildSystemBuses(storageFactory, dispatchers.system, resources);
|
|
932
|
+
const eventStore = this.createEventStore(storageFactory);
|
|
933
|
+
resources.eventStore = eventStore;
|
|
934
|
+
const eventBus = new EventBus(eventStore);
|
|
935
|
+
resources.eventBus = eventBus;
|
|
936
|
+
const publisher = new SignalPublisher(commandBus, eventBus, system.eventBus, this.#specSnapshot.name.value);
|
|
937
|
+
resources.publisher = publisher;
|
|
938
|
+
return {
|
|
939
|
+
commandBus,
|
|
940
|
+
eventStore,
|
|
941
|
+
systemEventStore: system.eventStore,
|
|
942
|
+
eventBus,
|
|
943
|
+
systemEventBus: system.eventBus,
|
|
944
|
+
publisher,
|
|
945
|
+
systemStand: system.stand,
|
|
946
|
+
systemSpec: system.spec,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
#buildSystemBuses(storageFactory, dispatchers, resources) {
|
|
950
|
+
const systemSpec = ContextParts.createSystemSpec(this.#specSnapshot, this.#persistSystemEvents);
|
|
951
|
+
const systemEventStore = systemSpec.storesEvents
|
|
952
|
+
? new EventStore(ContextParts.createStorageMode(systemSpec), storageFactory)
|
|
953
|
+
: undefined;
|
|
954
|
+
if (systemEventStore !== undefined)
|
|
955
|
+
resources.systemEventStore = systemEventStore;
|
|
956
|
+
const systemEventBus = eventBusAccess.createSystemBus(systemEventStore);
|
|
957
|
+
resources.systemEventBus = systemEventBus;
|
|
958
|
+
for (const dispatcher of dispatchers)
|
|
959
|
+
systemEventBus.register(dispatcher);
|
|
960
|
+
const systemStand = new Stand({
|
|
961
|
+
context: ContextParts.createStorageMode(systemSpec),
|
|
962
|
+
storageFactory,
|
|
963
|
+
});
|
|
964
|
+
resources.systemStand = systemStand;
|
|
965
|
+
return {
|
|
966
|
+
eventStore: systemEventStore,
|
|
967
|
+
eventBus: systemEventBus,
|
|
968
|
+
stand: systemStand,
|
|
969
|
+
spec: systemSpec,
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
#installStandalone(standalone, buses) {
|
|
973
|
+
ContextParts.assertUniqueCommandReceptors(standalone);
|
|
974
|
+
const runtime = standalone.length === 0
|
|
975
|
+
? undefined
|
|
976
|
+
: new StandaloneHandlerRuntime(ContextParts.matchStandaloneHandlers(standalone, this.#standaloneInstances(), buses.publisher));
|
|
977
|
+
const command = runtime?.commandDispatcher();
|
|
978
|
+
const event = runtime?.eventDispatcher();
|
|
979
|
+
const state = runtime?.stateDispatcher();
|
|
980
|
+
if (command !== undefined)
|
|
981
|
+
buses.commandBus.register(command);
|
|
982
|
+
if (event !== undefined)
|
|
983
|
+
buses.eventBus.register(event);
|
|
984
|
+
if (state !== undefined)
|
|
985
|
+
buses.systemEventBus.register(state);
|
|
986
|
+
return event;
|
|
987
|
+
}
|
|
988
|
+
#buildRuntime(storageFactory, buses, resources) {
|
|
989
|
+
const stand = new Stand({
|
|
990
|
+
context: ContextParts.createStorageMode(this.#specSnapshot),
|
|
991
|
+
storageFactory,
|
|
992
|
+
});
|
|
993
|
+
resources.stand = stand;
|
|
994
|
+
const registry = resources.registry ??
|
|
995
|
+
new StorageSubscriptionRegistry(ContextParts.createSubscriptionStorageContext(this.#specSnapshot), storageFactory);
|
|
996
|
+
resources.registry = registry;
|
|
997
|
+
const runtime = new SubscriptionRuntime(stand, buses.systemStand, buses.eventBus, buses.systemEventBus, registry);
|
|
998
|
+
resources.runtime = runtime;
|
|
999
|
+
return { stand, registry, runtime };
|
|
1000
|
+
}
|
|
1001
|
+
#createContext(repositories, storageFactory, buses, running) {
|
|
1002
|
+
return ContextParts.createBoundedContext(this.#specSnapshot, buses.commandBus, buses.eventBus, buses.systemEventBus, buses.publisher, running.stand, buses.systemStand, running.runtime, buses.systemSpec, storageFactory, repositories, this.#deliveryStrategy);
|
|
1003
|
+
}
|
|
1004
|
+
#failBuild(resources, error) {
|
|
1005
|
+
const cleanupErrors = [];
|
|
1006
|
+
ContextParts.attemptCleanup(() => resources.runtime?.abortClose(), cleanupErrors);
|
|
1007
|
+
ContextParts.attemptCleanup(() => resources.publisher?.abortAssembly(), cleanupErrors);
|
|
1008
|
+
if (resources.runtime === undefined) {
|
|
1009
|
+
ContextParts.attemptCleanup(() => void resources.registry?.close().catch(() => undefined), cleanupErrors);
|
|
1010
|
+
}
|
|
1011
|
+
ContextParts.attemptCleanup(() => void resources.stand?.close().catch(() => undefined), cleanupErrors);
|
|
1012
|
+
ContextParts.attemptCleanup(() => void resources.systemStand?.close().catch(() => undefined), cleanupErrors);
|
|
1013
|
+
ContextParts.cleanupBuildBuses(resources, cleanupErrors);
|
|
1014
|
+
if (cleanupErrors.length > 0) {
|
|
1015
|
+
throw new AggregateError([error, ...cleanupErrors], "Bounded Context build failed during cleanup.");
|
|
1016
|
+
}
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
async #loadGeneratedArtifacts(entityTypes) {
|
|
1020
|
+
if (entityTypes.length === 0 && this.#standaloneInstances().length === 0) {
|
|
1021
|
+
return Object.freeze({ repositories: Object.freeze([]), standalone: Object.freeze([]) });
|
|
884
1022
|
}
|
|
885
1023
|
const root = ContextParts.requireGeneratedRegistryRoot(this.#generatedRegistryRoot);
|
|
886
1024
|
const discovery = new GeneratedRegistryDiscovery();
|
|
@@ -896,8 +1034,19 @@ export class BoundedContextBuilder {
|
|
|
896
1034
|
throw error;
|
|
897
1035
|
}));
|
|
898
1036
|
const metadata = ContextParts.ingestGeneratedRegistries(registries);
|
|
899
|
-
|
|
1037
|
+
const repositories = Object.freeze(entityTypes.map((entityType) => ContextParts.createGeneratedRepository(entityType, registries, metadata, this.#generatedRepositoryOptions.get(entityType))));
|
|
1038
|
+
const standalone = Object.freeze(registries.flatMap((registry) => registry.receivers.filter((receiver) => receiver.receiverKind === "standalone")));
|
|
1039
|
+
return Object.freeze({ repositories, standalone });
|
|
1040
|
+
}
|
|
1041
|
+
#standaloneInstances() {
|
|
1042
|
+
return Object.freeze([...this.#assignees, ...this.#commanders, ...this.#eventReceivers]);
|
|
900
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Creates the domain EventStore using this context's storage mode.
|
|
1046
|
+
*
|
|
1047
|
+
* @param storageFactory Storage provider for event records.
|
|
1048
|
+
* @returns Domain EventStore.
|
|
1049
|
+
*/
|
|
901
1050
|
createEventStore(storageFactory) {
|
|
902
1051
|
return new EventStore(ContextParts.createStorageMode(this.#specSnapshot), storageFactory);
|
|
903
1052
|
}
|
|
@@ -914,7 +1063,7 @@ export class ContextSpec {
|
|
|
914
1063
|
constructContextSpec = (snapshot, token) => new ContextSpec(snapshot, token);
|
|
915
1064
|
}
|
|
916
1065
|
/**
|
|
917
|
-
* Creates a
|
|
1066
|
+
* Creates a context specification from prepared values.
|
|
918
1067
|
*
|
|
919
1068
|
* @param snapshot Contains immutable specification values.
|
|
920
1069
|
* @param token Proves framework-controlled construction.
|
|
@@ -966,10 +1115,19 @@ export class ContextSpec {
|
|
|
966
1115
|
}
|
|
967
1116
|
}
|
|
968
1117
|
const registeredRepositories = new WeakMap();
|
|
1118
|
+
/**
|
|
1119
|
+
* Reports failure while redispatching a stored event during read-side replay.
|
|
1120
|
+
*/
|
|
969
1121
|
class CatchUpReplayError extends Error {
|
|
970
1122
|
code = "READ_SIDE_CATCH_UP_REPLAY_FAILED";
|
|
971
1123
|
eventId;
|
|
972
1124
|
detail;
|
|
1125
|
+
/**
|
|
1126
|
+
* Creates a replay error tied to the stored event that failed.
|
|
1127
|
+
*
|
|
1128
|
+
* @param eventId Identifies the stored event.
|
|
1129
|
+
* @param detail Contains the bounded cause name and message.
|
|
1130
|
+
*/
|
|
973
1131
|
constructor(eventId, detail) {
|
|
974
1132
|
super(`Read-side catch-up failed for stored event "${eventId}".`);
|
|
975
1133
|
this.name = "ReadCatchUpReplayError";
|
|
@@ -982,6 +1140,63 @@ class CatchUpReplayError extends Error {
|
|
|
982
1140
|
* Assembles private bounded-context lifecycle and replay details.
|
|
983
1141
|
*/
|
|
984
1142
|
const ContextParts = Object.freeze({
|
|
1143
|
+
/**
|
|
1144
|
+
* Rejects duplicate command receptors in generated standalone groups.
|
|
1145
|
+
*
|
|
1146
|
+
* @param receivers Generated groups to validate.
|
|
1147
|
+
*/
|
|
1148
|
+
assertUniqueCommandReceptors(receivers) {
|
|
1149
|
+
const receptorByType = new Map();
|
|
1150
|
+
for (const receiver of receivers) {
|
|
1151
|
+
for (const handler of receiver.handlers) {
|
|
1152
|
+
if (handler.kind !== "command-assignment" && handler.kind !== "command-substitution")
|
|
1153
|
+
continue;
|
|
1154
|
+
const typeName = TypeUrls.derive(handler.input.schema);
|
|
1155
|
+
const prior = receptorByType.get(typeName);
|
|
1156
|
+
if (prior !== undefined)
|
|
1157
|
+
throw new Error(`Standalone command receptors conflict for "${typeName}": ${prior} and ${receiver.receiverType.name}.`);
|
|
1158
|
+
receptorByType.set(typeName, receiver.receiverType.name);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
/**
|
|
1163
|
+
* Matches generated standalone groups to their registered instances.
|
|
1164
|
+
*
|
|
1165
|
+
* @param generated Generated handler groups.
|
|
1166
|
+
* @param instances Registered standalone instances.
|
|
1167
|
+
* @param publisher Publisher used by bindings.
|
|
1168
|
+
* @returns Frozen bindings in generated order.
|
|
1169
|
+
*/
|
|
1170
|
+
matchStandaloneHandlers(generated, instances, publisher) {
|
|
1171
|
+
const byConstructor = new Map();
|
|
1172
|
+
for (const instance of instances) {
|
|
1173
|
+
const constructor = instance.constructor;
|
|
1174
|
+
if (byConstructor.has(constructor)) {
|
|
1175
|
+
throw new Error(`Standalone receiver ${constructor.name} is registered more than once.`);
|
|
1176
|
+
}
|
|
1177
|
+
byConstructor.set(constructor, instance);
|
|
1178
|
+
}
|
|
1179
|
+
const bindings = [];
|
|
1180
|
+
for (const receiver of generated) {
|
|
1181
|
+
const receiverType = receiver.receiverType;
|
|
1182
|
+
const instance = byConstructor.get(receiverType);
|
|
1183
|
+
if (instance === undefined) {
|
|
1184
|
+
throw new Error(`Generated standalone receiver ${receiverType.name} has no explicitly registered instance.`);
|
|
1185
|
+
}
|
|
1186
|
+
bindings.push(Object.freeze({ group: receiver, instance, publisher }));
|
|
1187
|
+
byConstructor.delete(receiverType);
|
|
1188
|
+
}
|
|
1189
|
+
if (byConstructor.size > 0) {
|
|
1190
|
+
throw new Error(`Registered standalone receiver ${[...byConstructor.keys()][0]?.name ?? "unknown"} has no generated metadata.`);
|
|
1191
|
+
}
|
|
1192
|
+
return Object.freeze(bindings);
|
|
1193
|
+
},
|
|
1194
|
+
/**
|
|
1195
|
+
* Calls cleanup and records any thrown error.
|
|
1196
|
+
*
|
|
1197
|
+
* @param onCleanup Cleanup action.
|
|
1198
|
+
* @param errors Collection receiving failures.
|
|
1199
|
+
*/
|
|
985
1200
|
attemptCleanup(onCleanup, errors) {
|
|
986
1201
|
try {
|
|
987
1202
|
onCleanup();
|
|
@@ -990,39 +1205,136 @@ const ContextParts = Object.freeze({
|
|
|
990
1205
|
errors.push(error);
|
|
991
1206
|
}
|
|
992
1207
|
},
|
|
1208
|
+
/**
|
|
1209
|
+
* Closes buses and stores after context assembly fails.
|
|
1210
|
+
*
|
|
1211
|
+
* @param resources Partially created resources.
|
|
1212
|
+
* @param errors Collection receiving failures.
|
|
1213
|
+
*/
|
|
1214
|
+
cleanupBuildBuses(resources, errors) {
|
|
1215
|
+
ContextParts.attemptCleanup(() => {
|
|
1216
|
+
if (resources.commandBus !== undefined)
|
|
1217
|
+
commandBusAccess.abortClose(resources.commandBus);
|
|
1218
|
+
}, errors);
|
|
1219
|
+
ContextParts.attemptCleanup(() => {
|
|
1220
|
+
if (resources.systemEventBus !== undefined)
|
|
1221
|
+
eventBusAccess.abortClose(resources.systemEventBus);
|
|
1222
|
+
else
|
|
1223
|
+
resources.systemEventStore?.close();
|
|
1224
|
+
}, errors);
|
|
1225
|
+
ContextParts.attemptCleanup(() => {
|
|
1226
|
+
if (resources.eventBus !== undefined)
|
|
1227
|
+
eventBusAccess.abortClose(resources.eventBus);
|
|
1228
|
+
else
|
|
1229
|
+
resources.eventStore?.close();
|
|
1230
|
+
}, errors);
|
|
1231
|
+
},
|
|
1232
|
+
/**
|
|
1233
|
+
* Returns constituent failures from nested aggregate errors.
|
|
1234
|
+
*
|
|
1235
|
+
* @param errors Errors to expand.
|
|
1236
|
+
* @returns Flattened error list.
|
|
1237
|
+
*/
|
|
993
1238
|
flattenErrors(errors) {
|
|
994
1239
|
return errors.flatMap((error) => error instanceof AggregateError ? ContextParts.flattenErrors(error.errors) : [error]);
|
|
995
1240
|
},
|
|
1241
|
+
/**
|
|
1242
|
+
* Rejects construction without the framework token.
|
|
1243
|
+
*
|
|
1244
|
+
* @param token Supplied token.
|
|
1245
|
+
* @param message Error message for an invalid token.
|
|
1246
|
+
*/
|
|
996
1247
|
requireFrameworkConstructionToken(token, message) {
|
|
997
1248
|
if (token !== frameworkConstructionToken) {
|
|
998
1249
|
throw new TypeError(message);
|
|
999
1250
|
}
|
|
1000
1251
|
},
|
|
1252
|
+
/**
|
|
1253
|
+
* Creates a context specification from its snapshot.
|
|
1254
|
+
*
|
|
1255
|
+
* @param specSnapshot Snapshot used for construction.
|
|
1256
|
+
* @returns Constructed context specification.
|
|
1257
|
+
*/
|
|
1001
1258
|
createContextSpec(specSnapshot) {
|
|
1002
1259
|
return constructContextSpec(specSnapshot, frameworkConstructionToken);
|
|
1003
1260
|
},
|
|
1261
|
+
/**
|
|
1262
|
+
* Creates a builder for a context snapshot.
|
|
1263
|
+
*
|
|
1264
|
+
* @param specSnapshot Context configuration snapshot.
|
|
1265
|
+
* @returns Constructed context builder.
|
|
1266
|
+
*/
|
|
1004
1267
|
createBoundedContextBuilder(specSnapshot) {
|
|
1005
1268
|
return constructBoundedContextBuilder(specSnapshot, frameworkConstructionToken);
|
|
1006
1269
|
},
|
|
1007
|
-
|
|
1270
|
+
/**
|
|
1271
|
+
* Creates a bounded context from assembled runtime components.
|
|
1272
|
+
*
|
|
1273
|
+
* @param specSnapshot Context configuration.
|
|
1274
|
+
* @param commandBus Command dispatch bus.
|
|
1275
|
+
* @param eventBus Domain event bus.
|
|
1276
|
+
* @param systemEventBus System event bus.
|
|
1277
|
+
* @param publisher Signal publisher.
|
|
1278
|
+
* @param stand Domain read-side Stand.
|
|
1279
|
+
* @param systemStand System read-side Stand.
|
|
1280
|
+
* @param runtime Subscription runtime.
|
|
1281
|
+
* @param systemSpec Paired System specification.
|
|
1282
|
+
* @param storageFactory Storage provider.
|
|
1283
|
+
* @param repositories Registered repository views.
|
|
1284
|
+
* @param deliveryStrategy Delivery sharding strategy.
|
|
1285
|
+
* @returns Constructed bounded context.
|
|
1286
|
+
*/
|
|
1287
|
+
createBoundedContext(specSnapshot, commandBus, eventBus, systemEventBus, publisher, stand, systemStand, runtime, systemSpec, storageFactory, repositories, deliveryStrategy) {
|
|
1008
1288
|
return constructBoundedContext({
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1289
|
+
snapshot: {
|
|
1290
|
+
name: specSnapshot.name,
|
|
1291
|
+
tenantMode: ContextParts.toTenantMode(specSnapshot.multitenant),
|
|
1292
|
+
spec: specSnapshot,
|
|
1293
|
+
},
|
|
1294
|
+
commandBus,
|
|
1295
|
+
eventBus,
|
|
1296
|
+
systemEventBus,
|
|
1297
|
+
publisher,
|
|
1298
|
+
stand,
|
|
1299
|
+
systemStand,
|
|
1300
|
+
subscriptionRuntime: runtime,
|
|
1301
|
+
systemSpec,
|
|
1302
|
+
storageFactory,
|
|
1303
|
+
repositories,
|
|
1304
|
+
deliveryStrategy,
|
|
1305
|
+
token: frameworkConstructionToken,
|
|
1306
|
+
});
|
|
1013
1307
|
},
|
|
1308
|
+
/**
|
|
1309
|
+
* Creates the storage mode represented by a context snapshot.
|
|
1310
|
+
*
|
|
1311
|
+
* @param specSnapshot Context configuration snapshot.
|
|
1312
|
+
* @returns Storage mode for the context.
|
|
1313
|
+
*/
|
|
1014
1314
|
createStorageMode(specSnapshot) {
|
|
1015
1315
|
return Object.freeze({
|
|
1016
1316
|
name: specSnapshot.name.value,
|
|
1017
1317
|
multitenant: specSnapshot.multitenant,
|
|
1018
1318
|
});
|
|
1019
1319
|
},
|
|
1320
|
+
/**
|
|
1321
|
+
* Creates a non-tenant-scoped subscription storage context.
|
|
1322
|
+
*
|
|
1323
|
+
* @param specSnapshot Context configuration snapshot.
|
|
1324
|
+
* @returns Subscription storage context.
|
|
1325
|
+
*/
|
|
1020
1326
|
createSubscriptionStorageContext(specSnapshot) {
|
|
1021
1327
|
return Object.freeze({
|
|
1022
1328
|
name: `${specSnapshot.name.value}:subscriptions`,
|
|
1023
1329
|
multitenant: false,
|
|
1024
1330
|
});
|
|
1025
1331
|
},
|
|
1332
|
+
/**
|
|
1333
|
+
* Captures the shard count and validates later shard results.
|
|
1334
|
+
*
|
|
1335
|
+
* @param strategy Configured delivery strategy.
|
|
1336
|
+
* @returns Immutable delivery strategy snapshot.
|
|
1337
|
+
*/
|
|
1026
1338
|
snapshotDeliveryStrategy(strategy) {
|
|
1027
1339
|
if (!Number.isSafeInteger(strategy.shardCount) || strategy.shardCount <= 0) {
|
|
1028
1340
|
throw new Error("Delivery strategy shard count must be a positive safe integer.");
|
|
@@ -1030,6 +1342,13 @@ const ContextParts = Object.freeze({
|
|
|
1030
1342
|
const shardCount = strategy.shardCount;
|
|
1031
1343
|
return Object.freeze({
|
|
1032
1344
|
shardCount,
|
|
1345
|
+
/**
|
|
1346
|
+
* Returns a target shard after verifying the captured shard total.
|
|
1347
|
+
*
|
|
1348
|
+
* @param targetId Inbox target identifier.
|
|
1349
|
+
* @param targetType Inbox target type.
|
|
1350
|
+
* @returns Validated shard index.
|
|
1351
|
+
*/
|
|
1033
1352
|
shardFor(targetId, targetType) {
|
|
1034
1353
|
const shard = strategy.shardFor(InboxTargets.clone(targetId), targetType);
|
|
1035
1354
|
if (shard.ofTotal !== shardCount) {
|
|
@@ -1039,6 +1358,13 @@ const ContextParts = Object.freeze({
|
|
|
1039
1358
|
},
|
|
1040
1359
|
});
|
|
1041
1360
|
},
|
|
1361
|
+
/**
|
|
1362
|
+
* Creates the storage context for read-side catch-up.
|
|
1363
|
+
*
|
|
1364
|
+
* @param specSnapshot Context configuration.
|
|
1365
|
+
* @param options Catch-up tenant options.
|
|
1366
|
+
* @returns Storage context for the selected scope.
|
|
1367
|
+
*/
|
|
1042
1368
|
catchUpStorageContext(specSnapshot, options) {
|
|
1043
1369
|
if (!specSnapshot.multitenant) {
|
|
1044
1370
|
if (options.tenantId !== undefined) {
|
|
@@ -1056,6 +1382,12 @@ const ContextParts = Object.freeze({
|
|
|
1056
1382
|
tenantId: TenantBoundary.from(tenantId).tenantId,
|
|
1057
1383
|
});
|
|
1058
1384
|
},
|
|
1385
|
+
/**
|
|
1386
|
+
* Creates a bounded-context name after checking reserved names.
|
|
1387
|
+
*
|
|
1388
|
+
* @param value Candidate context name.
|
|
1389
|
+
* @returns Immutable validated name.
|
|
1390
|
+
*/
|
|
1059
1391
|
createBoundedContextName(value) {
|
|
1060
1392
|
if (typeof value !== "string" ||
|
|
1061
1393
|
value.trim().length === 0 ||
|
|
@@ -1064,6 +1396,13 @@ const ContextParts = Object.freeze({
|
|
|
1064
1396
|
}
|
|
1065
1397
|
return Object.freeze({ value });
|
|
1066
1398
|
},
|
|
1399
|
+
/**
|
|
1400
|
+
* Creates a context snapshot with event storage enabled.
|
|
1401
|
+
*
|
|
1402
|
+
* @param name Context name.
|
|
1403
|
+
* @param multitenant Whether tenant isolation is enabled.
|
|
1404
|
+
* @returns Immutable specification snapshot.
|
|
1405
|
+
*/
|
|
1067
1406
|
createSpecSnapshot(name, multitenant) {
|
|
1068
1407
|
return Object.freeze({
|
|
1069
1408
|
name: ContextParts.createBoundedContextName(name),
|
|
@@ -1071,9 +1410,21 @@ const ContextParts = Object.freeze({
|
|
|
1071
1410
|
storesEvents: true,
|
|
1072
1411
|
});
|
|
1073
1412
|
},
|
|
1413
|
+
/**
|
|
1414
|
+
* Copies a bounded-context name into an immutable value.
|
|
1415
|
+
*
|
|
1416
|
+
* @param name Source context name.
|
|
1417
|
+
* @returns Immutable name copy.
|
|
1418
|
+
*/
|
|
1074
1419
|
cloneName(name) {
|
|
1075
1420
|
return ContextParts.createBoundedContextName(name.value);
|
|
1076
1421
|
},
|
|
1422
|
+
/**
|
|
1423
|
+
* Copies a context specification snapshot and its name.
|
|
1424
|
+
*
|
|
1425
|
+
* @param spec Source specification.
|
|
1426
|
+
* @returns Immutable specification copy.
|
|
1427
|
+
*/
|
|
1077
1428
|
cloneSpecSnapshot(spec) {
|
|
1078
1429
|
return Object.freeze({
|
|
1079
1430
|
name: ContextParts.cloneName(spec.name),
|
|
@@ -1081,6 +1432,12 @@ const ContextParts = Object.freeze({
|
|
|
1081
1432
|
storesEvents: spec.storesEvents,
|
|
1082
1433
|
});
|
|
1083
1434
|
},
|
|
1435
|
+
/**
|
|
1436
|
+
* Copies a bounded-context snapshot and its specification.
|
|
1437
|
+
*
|
|
1438
|
+
* @param snapshot Source context snapshot.
|
|
1439
|
+
* @returns Immutable context snapshot copy.
|
|
1440
|
+
*/
|
|
1084
1441
|
cloneContextSnapshot(snapshot) {
|
|
1085
1442
|
return Object.freeze({
|
|
1086
1443
|
name: ContextParts.cloneName(snapshot.name),
|
|
@@ -1088,6 +1445,13 @@ const ContextParts = Object.freeze({
|
|
|
1088
1445
|
spec: ContextParts.cloneSpecSnapshot(snapshot.spec),
|
|
1089
1446
|
});
|
|
1090
1447
|
},
|
|
1448
|
+
/**
|
|
1449
|
+
* Creates the System specification paired with a domain context.
|
|
1450
|
+
*
|
|
1451
|
+
* @param domainSpec Domain specification.
|
|
1452
|
+
* @param storesEvents Whether System events are stored.
|
|
1453
|
+
* @returns System context snapshot.
|
|
1454
|
+
*/
|
|
1091
1455
|
createSystemSpec(domainSpec, storesEvents) {
|
|
1092
1456
|
return Object.freeze({
|
|
1093
1457
|
name: ContextParts.createBoundedContextName(`${domainSpec.name.value}_System`),
|
|
@@ -1095,24 +1459,49 @@ const ContextParts = Object.freeze({
|
|
|
1095
1459
|
storesEvents,
|
|
1096
1460
|
});
|
|
1097
1461
|
},
|
|
1462
|
+
/**
|
|
1463
|
+
* Creates a pairing snapshot for domain and System specifications.
|
|
1464
|
+
*
|
|
1465
|
+
* @param snapshot Domain snapshot.
|
|
1466
|
+
* @param systemSpec Paired System specification.
|
|
1467
|
+
* @returns Immutable context pairing.
|
|
1468
|
+
*/
|
|
1098
1469
|
createSystemPairing(snapshot, systemSpec) {
|
|
1099
1470
|
return Object.freeze({
|
|
1100
1471
|
domain: ContextParts.cloneContextSnapshot(snapshot),
|
|
1101
1472
|
system: ContextParts.cloneSpecSnapshot(systemSpec),
|
|
1102
1473
|
});
|
|
1103
1474
|
},
|
|
1475
|
+
/**
|
|
1476
|
+
* Copies both specifications in a System pairing.
|
|
1477
|
+
*
|
|
1478
|
+
* @param pairing Pairing to copy.
|
|
1479
|
+
* @returns Immutable pairing copy.
|
|
1480
|
+
*/
|
|
1104
1481
|
cloneSystemPairing(pairing) {
|
|
1105
1482
|
return Object.freeze({
|
|
1106
1483
|
domain: ContextParts.cloneContextSnapshot(pairing.domain),
|
|
1107
1484
|
system: ContextParts.cloneSpecSnapshot(pairing.system),
|
|
1108
1485
|
});
|
|
1109
1486
|
},
|
|
1487
|
+
/**
|
|
1488
|
+
* Lists non-internal event type URLs exposed by an event bus.
|
|
1489
|
+
*
|
|
1490
|
+
* @param eventBus Bus whose schemas are inspected.
|
|
1491
|
+
* @returns Frozen exposed type URLs.
|
|
1492
|
+
*/
|
|
1110
1493
|
exposedEventTypeUrls(eventBus) {
|
|
1111
1494
|
return Object.freeze(eventBusAccess
|
|
1112
1495
|
.eventSchemas(eventBus)
|
|
1113
1496
|
.filter((schema) => !ContextParts.isInternalEventSchema(schema))
|
|
1114
1497
|
.map((schema) => TypeUrls.derive(schema)));
|
|
1115
1498
|
},
|
|
1499
|
+
/**
|
|
1500
|
+
* Collects external event schemas by type URL.
|
|
1501
|
+
*
|
|
1502
|
+
* @param dispatchers Dispatchers to inspect.
|
|
1503
|
+
* @returns Unique frozen schemas.
|
|
1504
|
+
*/
|
|
1116
1505
|
externalEventSchemas(dispatchers) {
|
|
1117
1506
|
const schemas = new Map();
|
|
1118
1507
|
for (const dispatcher of dispatchers) {
|
|
@@ -1122,6 +1511,14 @@ const ContextParts = Object.freeze({
|
|
|
1122
1511
|
}
|
|
1123
1512
|
return Object.freeze([...schemas.values()]);
|
|
1124
1513
|
},
|
|
1514
|
+
/**
|
|
1515
|
+
* Opens the integration broker and associates context readiness.
|
|
1516
|
+
*
|
|
1517
|
+
* @param context Context receiving integration.
|
|
1518
|
+
* @param eventBus Event bus supplied to broker.
|
|
1519
|
+
* @param systemSpec Paired System specification.
|
|
1520
|
+
* @param externalEventSchemas Schemas accepted from outside.
|
|
1521
|
+
*/
|
|
1125
1522
|
attachIntegration(context, eventBus, systemSpec, externalEventSchemas) {
|
|
1126
1523
|
const broker = new IntegrationBroker({
|
|
1127
1524
|
contextName: create(BoundedContextNameSchema, { value: context.name.value }),
|
|
@@ -1144,6 +1541,13 @@ const ContextParts = Object.freeze({
|
|
|
1144
1541
|
void ready.catch(() => undefined);
|
|
1145
1542
|
contextIntegrations.set(context, { broker, ready });
|
|
1146
1543
|
},
|
|
1544
|
+
/**
|
|
1545
|
+
* Posts an event after integration readiness and tenant validation.
|
|
1546
|
+
*
|
|
1547
|
+
* @param context Target context.
|
|
1548
|
+
* @param event Event to post.
|
|
1549
|
+
* @returns Promise fulfilled after posting.
|
|
1550
|
+
*/
|
|
1147
1551
|
postContextEvent(context, event) {
|
|
1148
1552
|
const buses = contextEventBuses.get(context);
|
|
1149
1553
|
if (buses === undefined)
|
|
@@ -1155,15 +1559,34 @@ const ContextParts = Object.freeze({
|
|
|
1155
1559
|
return buses[0].post(event);
|
|
1156
1560
|
});
|
|
1157
1561
|
},
|
|
1562
|
+
/**
|
|
1563
|
+
* Closes the context integration broker.
|
|
1564
|
+
*
|
|
1565
|
+
* @param context Context whose broker is closed.
|
|
1566
|
+
* @returns Promise fulfilled after closing.
|
|
1567
|
+
*/
|
|
1158
1568
|
closeIntegration(context) {
|
|
1159
1569
|
return contextIntegrations.get(context)?.broker.close() ?? Promise.resolve();
|
|
1160
1570
|
},
|
|
1571
|
+
/**
|
|
1572
|
+
* Publishes an imported event through the integration broker.
|
|
1573
|
+
*
|
|
1574
|
+
* @param context Context integration.
|
|
1575
|
+
* @param event Imported event.
|
|
1576
|
+
* @returns Promise fulfilled after publishing.
|
|
1577
|
+
*/
|
|
1161
1578
|
publishImported(context, event) {
|
|
1162
1579
|
const integration = contextIntegrations.get(context);
|
|
1163
1580
|
if (integration === undefined)
|
|
1164
1581
|
return Promise.reject(new Error("Context integration is unavailable."));
|
|
1165
1582
|
return integration.ready.then(() => integration.broker.publishImported(event));
|
|
1166
1583
|
},
|
|
1584
|
+
/**
|
|
1585
|
+
* Checks imported event tenant against the context tenant mode.
|
|
1586
|
+
*
|
|
1587
|
+
* @param context Target context.
|
|
1588
|
+
* @param event Imported event to validate.
|
|
1589
|
+
*/
|
|
1167
1590
|
validateImportedTenant(context, event) {
|
|
1168
1591
|
const tenantId = ContextParts.readReplayTenant(event);
|
|
1169
1592
|
if (!context.isMultitenant) {
|
|
@@ -1177,17 +1600,41 @@ const ContextParts = Object.freeze({
|
|
|
1177
1600
|
}
|
|
1178
1601
|
TenantBoundary.from(tenantId);
|
|
1179
1602
|
},
|
|
1603
|
+
/**
|
|
1604
|
+
* Waits for the integration broker to become ready.
|
|
1605
|
+
*
|
|
1606
|
+
* @param context Context whose readiness is observed.
|
|
1607
|
+
* @returns Readiness promise.
|
|
1608
|
+
*/
|
|
1180
1609
|
integrationReady(context) {
|
|
1181
1610
|
return contextIntegrations.get(context)?.ready ?? Promise.resolve();
|
|
1182
1611
|
},
|
|
1612
|
+
/**
|
|
1613
|
+
* Checks whether a schema carries an internal or SPI marker.
|
|
1614
|
+
*
|
|
1615
|
+
* @param schema Event schema to inspect.
|
|
1616
|
+
* @returns True when an internal marker is set.
|
|
1617
|
+
*/
|
|
1183
1618
|
isInternalEventSchema(schema) {
|
|
1184
1619
|
return ((hasOption(schema, internal_type) && getOption(schema, internal_type)) ||
|
|
1185
1620
|
(hasOption(schema, SPI_type) && getOption(schema, SPI_type)) ||
|
|
1186
1621
|
(hasOption(schema.file, internal_all) && getOption(schema.file, internal_all)));
|
|
1187
1622
|
},
|
|
1623
|
+
/**
|
|
1624
|
+
* Converts the multitenant flag to a tenant mode.
|
|
1625
|
+
*
|
|
1626
|
+
* @param multitenant Context isolation flag.
|
|
1627
|
+
* @returns Corresponding tenant mode.
|
|
1628
|
+
*/
|
|
1188
1629
|
toTenantMode(multitenant) {
|
|
1189
1630
|
return multitenant ? "multitenant" : "single-tenant";
|
|
1190
1631
|
},
|
|
1632
|
+
/**
|
|
1633
|
+
* Reads the System pairing recorded for a built context.
|
|
1634
|
+
*
|
|
1635
|
+
* @param context Context to inspect.
|
|
1636
|
+
* @returns Pairing snapshot; throws if unpaired.
|
|
1637
|
+
*/
|
|
1191
1638
|
requireSystemPairing(context) {
|
|
1192
1639
|
const pairing = contextSystemPairings.get(context);
|
|
1193
1640
|
if (pairing === undefined) {
|
|
@@ -1195,6 +1642,12 @@ const ContextParts = Object.freeze({
|
|
|
1195
1642
|
}
|
|
1196
1643
|
return pairing;
|
|
1197
1644
|
},
|
|
1645
|
+
/**
|
|
1646
|
+
* Reads the tenant index recorded for a built context.
|
|
1647
|
+
*
|
|
1648
|
+
* @param context Context to inspect.
|
|
1649
|
+
* @returns Tenant index; throws if unavailable.
|
|
1650
|
+
*/
|
|
1198
1651
|
requireTenantIndex(context) {
|
|
1199
1652
|
const tenantIndex = contextTenantIndexes.get(context);
|
|
1200
1653
|
if (tenantIndex === undefined) {
|
|
@@ -1202,16 +1655,33 @@ const ContextParts = Object.freeze({
|
|
|
1202
1655
|
}
|
|
1203
1656
|
return tenantIndex;
|
|
1204
1657
|
},
|
|
1658
|
+
/**
|
|
1659
|
+
* Rejects values that are not Repository instances.
|
|
1660
|
+
*
|
|
1661
|
+
* @param repository Candidate value.
|
|
1662
|
+
* @param operation Operation name included in the error.
|
|
1663
|
+
*/
|
|
1205
1664
|
requireRepositoryInstance(repository, operation) {
|
|
1206
1665
|
if (!repositoryAccess.hasInstance(repository)) {
|
|
1207
1666
|
throw new TypeError(`${operation} requires a Repository instance.`);
|
|
1208
1667
|
}
|
|
1209
1668
|
},
|
|
1669
|
+
/**
|
|
1670
|
+
* Rejects values that are not Entity classes.
|
|
1671
|
+
*
|
|
1672
|
+
* @param entityType Candidate Entity constructor.
|
|
1673
|
+
* @param operation Operation name included in the error.
|
|
1674
|
+
*/
|
|
1210
1675
|
requireEntityClass(entityType, operation) {
|
|
1211
1676
|
if (typeof entityType !== "function") {
|
|
1212
1677
|
throw new TypeError(`${operation} requires a Repository instance. Use an entity class only with buildAsync().`);
|
|
1213
1678
|
}
|
|
1214
1679
|
},
|
|
1680
|
+
/**
|
|
1681
|
+
* Rejects synchronous assembly when classes need generated metadata.
|
|
1682
|
+
*
|
|
1683
|
+
* @param entityTypes Entity classes to assemble.
|
|
1684
|
+
*/
|
|
1215
1685
|
rejectSyncEntityAssembly(entityTypes) {
|
|
1216
1686
|
if (entityTypes.size === 0) {
|
|
1217
1687
|
return;
|
|
@@ -1219,6 +1689,12 @@ const ContextParts = Object.freeze({
|
|
|
1219
1689
|
throw new Error("BoundedContextBuilder.build() cannot assemble entity classes from generated metadata. " +
|
|
1220
1690
|
"Use buildAsync().");
|
|
1221
1691
|
},
|
|
1692
|
+
/**
|
|
1693
|
+
* Validates that a root exists when generated metadata must be loaded.
|
|
1694
|
+
*
|
|
1695
|
+
* @param root Configured registry location.
|
|
1696
|
+
* @returns Configured root; throws when absent.
|
|
1697
|
+
*/
|
|
1222
1698
|
requireGeneratedRegistryRoot(root) {
|
|
1223
1699
|
if (root !== undefined) {
|
|
1224
1700
|
return root;
|
|
@@ -1226,6 +1702,12 @@ const ContextParts = Object.freeze({
|
|
|
1226
1702
|
throw new Error("BoundedContextBuilder.buildAsync() requires withGeneratedRegistryRoot(root) " +
|
|
1227
1703
|
"when assembling entity classes from generated metadata.");
|
|
1228
1704
|
},
|
|
1705
|
+
/**
|
|
1706
|
+
* Resolves the generated registry module and verifies it stays beneath the configured root.
|
|
1707
|
+
*
|
|
1708
|
+
* @param root Configured filesystem path or file URL.
|
|
1709
|
+
* @returns Registry module file URL.
|
|
1710
|
+
*/
|
|
1229
1711
|
async trustedGeneratedRegistryModule(root) {
|
|
1230
1712
|
const trustedRoot = await ContextParts.canonicalGeneratedRegistryRoot(root);
|
|
1231
1713
|
const registryPath = resolve(trustedRoot, generatedRegistryFile);
|
|
@@ -1236,6 +1718,12 @@ const ContextParts = Object.freeze({
|
|
|
1236
1718
|
}
|
|
1237
1719
|
return pathToFileURL(canonicalRegistryPath);
|
|
1238
1720
|
},
|
|
1721
|
+
/**
|
|
1722
|
+
* Resolves a generated registry root to its canonical filesystem path.
|
|
1723
|
+
*
|
|
1724
|
+
* @param root Configured path or file URL.
|
|
1725
|
+
* @returns Canonical directory path.
|
|
1726
|
+
*/
|
|
1239
1727
|
async canonicalGeneratedRegistryRoot(root) {
|
|
1240
1728
|
const rootPath = ContextParts.generatedRegistryRootPath(root);
|
|
1241
1729
|
try {
|
|
@@ -1245,6 +1733,12 @@ const ContextParts = Object.freeze({
|
|
|
1245
1733
|
throw new Error(`Generated registry root "${rootPath}" must be an existing readable directory.`, { cause: error });
|
|
1246
1734
|
}
|
|
1247
1735
|
},
|
|
1736
|
+
/**
|
|
1737
|
+
* Converts a registry root path or file URL to an absolute filesystem path.
|
|
1738
|
+
*
|
|
1739
|
+
* @param root Configured path or file URL.
|
|
1740
|
+
* @returns Absolute filesystem path.
|
|
1741
|
+
*/
|
|
1248
1742
|
generatedRegistryRootPath(root) {
|
|
1249
1743
|
if (root instanceof URL) {
|
|
1250
1744
|
return ContextParts.fileUrlPath(root, "Generated registry root");
|
|
@@ -1254,6 +1748,13 @@ const ContextParts = Object.freeze({
|
|
|
1254
1748
|
}
|
|
1255
1749
|
return resolve(root);
|
|
1256
1750
|
},
|
|
1751
|
+
/**
|
|
1752
|
+
* Validates a file URL and converts it to an absolute filesystem path.
|
|
1753
|
+
*
|
|
1754
|
+
* @param url File URL to convert.
|
|
1755
|
+
* @param label Label used in validation errors.
|
|
1756
|
+
* @returns Filesystem path.
|
|
1757
|
+
*/
|
|
1257
1758
|
fileUrlPath(url, label) {
|
|
1258
1759
|
if (url.protocol !== "file:") {
|
|
1259
1760
|
throw new Error(`${label} "${url.href}" must use the file: URL scheme.`);
|
|
@@ -1263,6 +1764,12 @@ const ContextParts = Object.freeze({
|
|
|
1263
1764
|
}
|
|
1264
1765
|
return resolve(fileURLToPath(url));
|
|
1265
1766
|
},
|
|
1767
|
+
/**
|
|
1768
|
+
* Parses a generated registry root string as a URL.
|
|
1769
|
+
*
|
|
1770
|
+
* @param root Registry root string.
|
|
1771
|
+
* @returns Parsed URL.
|
|
1772
|
+
*/
|
|
1266
1773
|
parseRootUrl(root) {
|
|
1267
1774
|
try {
|
|
1268
1775
|
return new URL(root);
|
|
@@ -1271,9 +1778,21 @@ const ContextParts = Object.freeze({
|
|
|
1271
1778
|
throw new Error(`Generated registry root "${root}" is not a valid URL.`, { cause: error });
|
|
1272
1779
|
}
|
|
1273
1780
|
},
|
|
1781
|
+
/**
|
|
1782
|
+
* Checks strings for a URL scheme, excluding Windows drive paths.
|
|
1783
|
+
*
|
|
1784
|
+
* @param value String to inspect.
|
|
1785
|
+
* @returns True when the string has a URL scheme.
|
|
1786
|
+
*/
|
|
1274
1787
|
isUrlLike(value) {
|
|
1275
1788
|
return moduleSchemeRe.test(value) && !/^[A-Za-z]:[\\/]/.test(value);
|
|
1276
1789
|
},
|
|
1790
|
+
/**
|
|
1791
|
+
* Verifies registry-module readability and resolves its canonical path.
|
|
1792
|
+
*
|
|
1793
|
+
* @param registryPath Registry module filesystem path.
|
|
1794
|
+
* @returns Canonical readable path.
|
|
1795
|
+
*/
|
|
1277
1796
|
async canonicalReadableRegistryPath(registryPath) {
|
|
1278
1797
|
try {
|
|
1279
1798
|
await access(registryPath, fsConstants.R_OK);
|
|
@@ -1283,6 +1802,13 @@ const ContextParts = Object.freeze({
|
|
|
1283
1802
|
throw new Error(`Generated handler registry module "${registryPath}" must exist and be readable.`, { cause: error });
|
|
1284
1803
|
}
|
|
1285
1804
|
},
|
|
1805
|
+
/**
|
|
1806
|
+
* Checks whether a canonical path escapes a canonical root.
|
|
1807
|
+
*
|
|
1808
|
+
* @param canonicalRoot Root directory.
|
|
1809
|
+
* @param canonicalPath Candidate path.
|
|
1810
|
+
* @returns True when the candidate escapes the root.
|
|
1811
|
+
*/
|
|
1286
1812
|
resolvesOutsideRoot(canonicalRoot, canonicalPath) {
|
|
1287
1813
|
const relativePath = relative(canonicalRoot, canonicalPath);
|
|
1288
1814
|
return (relativePath.startsWith("..") ||
|
|
@@ -1290,13 +1816,30 @@ const ContextParts = Object.freeze({
|
|
|
1290
1816
|
relativePath.split(sep).includes("..") ||
|
|
1291
1817
|
isAbsolute(relativePath));
|
|
1292
1818
|
},
|
|
1819
|
+
/**
|
|
1820
|
+
* Creates a cache-bust query after a prior registry load failure.
|
|
1821
|
+
*
|
|
1822
|
+
* @param registryKey Canonical registry identifier.
|
|
1823
|
+
* @returns Retry query, or an empty object on the first attempt.
|
|
1824
|
+
*/
|
|
1293
1825
|
generatedRegistryCacheBust(registryKey) {
|
|
1294
1826
|
const attempt = generatedRegistryLoadAttempts.get(registryKey) ?? 0;
|
|
1295
1827
|
return attempt === 0 ? {} : { cacheBust: `retry-${attempt.toString()}` };
|
|
1296
1828
|
},
|
|
1829
|
+
/**
|
|
1830
|
+
* Records another failed load for a registry.
|
|
1831
|
+
*
|
|
1832
|
+
* @param registryKey Canonical registry identifier.
|
|
1833
|
+
*/
|
|
1297
1834
|
recordGeneratedRegistryFailure(registryKey) {
|
|
1298
1835
|
generatedRegistryLoadAttempts.set(registryKey, (generatedRegistryLoadAttempts.get(registryKey) ?? 0) + 1);
|
|
1299
1836
|
},
|
|
1837
|
+
/**
|
|
1838
|
+
* Loads generated handler registries into a metadata registry.
|
|
1839
|
+
*
|
|
1840
|
+
* @param registries Generated registries to ingest.
|
|
1841
|
+
* @returns Populated handler metadata registry.
|
|
1842
|
+
*/
|
|
1300
1843
|
ingestGeneratedRegistries(registries) {
|
|
1301
1844
|
const registry = new HandlerMetadataRegistry();
|
|
1302
1845
|
const ingestor = new HandlerRegistryIngestor();
|
|
@@ -1305,6 +1848,15 @@ const ContextParts = Object.freeze({
|
|
|
1305
1848
|
}
|
|
1306
1849
|
return registry;
|
|
1307
1850
|
},
|
|
1851
|
+
/**
|
|
1852
|
+
* Creates a repository from generated Entity handler metadata.
|
|
1853
|
+
*
|
|
1854
|
+
* @param entityType Entity class.
|
|
1855
|
+
* @param registries Generated registries to search.
|
|
1856
|
+
* @param metadata Ingested handler metadata.
|
|
1857
|
+
* @param options Repository options overriding defaults.
|
|
1858
|
+
* @returns Configured repository view.
|
|
1859
|
+
*/
|
|
1308
1860
|
createGeneratedRepository(entityType, registries, metadata, options) {
|
|
1309
1861
|
const generated = ContextParts.findGeneratedEntity(entityType, registries);
|
|
1310
1862
|
const handlers = ContextParts.findGeneratedHandlers(entityType, generated, metadata);
|
|
@@ -1316,15 +1868,30 @@ const ContextParts = Object.freeze({
|
|
|
1316
1868
|
}, options);
|
|
1317
1869
|
return new Repository(repositoryOptions);
|
|
1318
1870
|
},
|
|
1871
|
+
/**
|
|
1872
|
+
* Finds the generated Entity handler group for an Entity class.
|
|
1873
|
+
*
|
|
1874
|
+
* @param entityType Entity class to find.
|
|
1875
|
+
* @param registries Generated registries to search.
|
|
1876
|
+
* @returns Matching handler group; throws when absent.
|
|
1877
|
+
*/
|
|
1319
1878
|
findGeneratedEntity(entityType, registries) {
|
|
1320
1879
|
for (const registry of registries) {
|
|
1321
|
-
const generated = registry.
|
|
1880
|
+
const generated = registry.receivers.find((receiver) => receiver.receiverKind === "entity" && receiver.receiverType === entityType);
|
|
1322
1881
|
if (generated !== undefined) {
|
|
1323
1882
|
return generated;
|
|
1324
1883
|
}
|
|
1325
1884
|
}
|
|
1326
1885
|
throw new Error(`Generated handler registry is missing metadata for ${entityType.name}.`);
|
|
1327
1886
|
},
|
|
1887
|
+
/**
|
|
1888
|
+
* Finds handler metadata for an Entity class and state schema.
|
|
1889
|
+
*
|
|
1890
|
+
* @param entityType Entity class.
|
|
1891
|
+
* @param generated Matching generated Entity group.
|
|
1892
|
+
* @param metadata Handler metadata registry.
|
|
1893
|
+
* @returns Matching handler metadata; throws when absent.
|
|
1894
|
+
*/
|
|
1328
1895
|
findGeneratedHandlers(entityType, generated, metadata) {
|
|
1329
1896
|
const matches = metadata.findByState(generated.stateSchema.typeName);
|
|
1330
1897
|
const handlers = matches.find((candidate) => candidate.entityType === entityType);
|
|
@@ -1333,11 +1900,45 @@ const ContextParts = Object.freeze({
|
|
|
1333
1900
|
}
|
|
1334
1901
|
return handlers;
|
|
1335
1902
|
},
|
|
1903
|
+
/**
|
|
1904
|
+
* Collects event schemas declared as returned or thrown handler outcomes.
|
|
1905
|
+
*
|
|
1906
|
+
* @param generated Generated Entity handler group.
|
|
1907
|
+
* @returns Frozen unique event schemas.
|
|
1908
|
+
*/
|
|
1336
1909
|
aggregateAssignedEvents(generated) {
|
|
1337
|
-
return ContextParts.uniqueSchemas(generated.handlers.flatMap((handler) =>
|
|
1338
|
-
|
|
1339
|
-
|
|
1910
|
+
return ContextParts.uniqueSchemas(generated.handlers.flatMap((handler) => [
|
|
1911
|
+
...(handler.kind === "command-assignment" || handler.kind === "event-reaction"
|
|
1912
|
+
? handler.outcomes.returned
|
|
1913
|
+
: []),
|
|
1914
|
+
...(handler.kind === "command-assignment" || handler.kind === "command-substitution"
|
|
1915
|
+
? handler.outcomes.thrown
|
|
1916
|
+
: []),
|
|
1917
|
+
]));
|
|
1918
|
+
},
|
|
1919
|
+
/**
|
|
1920
|
+
* Collects event schemas produced by standalone handler outcomes.
|
|
1921
|
+
*
|
|
1922
|
+
* @param receivers Generated standalone groups.
|
|
1923
|
+
* @returns Frozen unique event schemas.
|
|
1924
|
+
*/
|
|
1925
|
+
standaloneProducedEventSchemas(receivers) {
|
|
1926
|
+
return ContextParts.uniqueSchemas(receivers.flatMap((receiver) => receiver.handlers.flatMap((handler) => [
|
|
1927
|
+
...(handler.kind === "command-assignment" || handler.kind === "event-reaction"
|
|
1928
|
+
? handler.outcomes.returned
|
|
1929
|
+
: []),
|
|
1930
|
+
...(handler.kind === "command-assignment" || handler.kind === "command-substitution"
|
|
1931
|
+
? handler.outcomes.thrown
|
|
1932
|
+
: []),
|
|
1933
|
+
])));
|
|
1340
1934
|
},
|
|
1935
|
+
/**
|
|
1936
|
+
* Returns unique schemas by Protobuf type name, preserving the last schema.
|
|
1937
|
+
*
|
|
1938
|
+
* @typeParam Schema Descriptor schema type retained in the result.
|
|
1939
|
+
* @param schemas Schemas to deduplicate.
|
|
1940
|
+
* @returns Frozen deduplicated schemas.
|
|
1941
|
+
*/
|
|
1341
1942
|
uniqueSchemas(schemas) {
|
|
1342
1943
|
const byTypeName = new Map();
|
|
1343
1944
|
for (const schema of schemas) {
|
|
@@ -1345,6 +1946,11 @@ const ContextParts = Object.freeze({
|
|
|
1345
1946
|
}
|
|
1346
1947
|
return Object.freeze([...byTypeName.values()]);
|
|
1347
1948
|
},
|
|
1949
|
+
/**
|
|
1950
|
+
* Validates repository uniqueness, registration state, and projection cycles.
|
|
1951
|
+
*
|
|
1952
|
+
* @param repositories Repository views to validate.
|
|
1953
|
+
*/
|
|
1348
1954
|
preflightRepositories(repositories) {
|
|
1349
1955
|
const entityTypes = new Set();
|
|
1350
1956
|
const stateTypeNames = new Set();
|
|
@@ -1367,6 +1973,11 @@ const ContextParts = Object.freeze({
|
|
|
1367
1973
|
}
|
|
1368
1974
|
ContextParts.rejectStateCycles(repositories);
|
|
1369
1975
|
},
|
|
1976
|
+
/**
|
|
1977
|
+
* Rejects cycles among repository state-subscription dependencies.
|
|
1978
|
+
*
|
|
1979
|
+
* @param repositories Repository views whose dependencies are checked.
|
|
1980
|
+
*/
|
|
1370
1981
|
rejectStateCycles(repositories) {
|
|
1371
1982
|
const dependencies = new Map();
|
|
1372
1983
|
for (const repository of repositories) {
|
|
@@ -1397,30 +2008,66 @@ const ContextParts = Object.freeze({
|
|
|
1397
2008
|
for (const stateType of dependencies.keys())
|
|
1398
2009
|
visit(stateType);
|
|
1399
2010
|
},
|
|
2011
|
+
/**
|
|
2012
|
+
* Collects command dispatchers configured by repositories.
|
|
2013
|
+
*
|
|
2014
|
+
* @param repositories Repository views to inspect.
|
|
2015
|
+
* @returns Present command dispatchers.
|
|
2016
|
+
*/
|
|
1400
2017
|
repositoryCommandDispatchers(repositories) {
|
|
1401
2018
|
return repositories.flatMap((repository) => {
|
|
1402
2019
|
const dispatcher = repositoryAccess.commandDispatcher(repository);
|
|
1403
2020
|
return dispatcher === undefined ? [] : [dispatcher];
|
|
1404
2021
|
});
|
|
1405
2022
|
},
|
|
2023
|
+
/**
|
|
2024
|
+
* Collects event dispatchers configured by repositories.
|
|
2025
|
+
*
|
|
2026
|
+
* @param repositories Repository views to inspect.
|
|
2027
|
+
* @returns Present event dispatchers.
|
|
2028
|
+
*/
|
|
1406
2029
|
repositoryEventDispatchers(repositories) {
|
|
1407
2030
|
return repositories.flatMap((repository) => {
|
|
1408
2031
|
const dispatcher = repositoryAccess.eventDispatcher(repository);
|
|
1409
2032
|
return dispatcher === undefined ? [] : [dispatcher];
|
|
1410
2033
|
});
|
|
1411
2034
|
},
|
|
2035
|
+
/**
|
|
2036
|
+
* Collects System event dispatchers configured by repositories.
|
|
2037
|
+
*
|
|
2038
|
+
* @param repositories Repository views to inspect.
|
|
2039
|
+
* @returns Present System event dispatchers.
|
|
2040
|
+
*/
|
|
1412
2041
|
repositorySystemEventDispatchers(repositories) {
|
|
1413
2042
|
return repositories.flatMap((repository) => {
|
|
1414
2043
|
const dispatcher = repositoryAccess.systemEventDispatcher(repository);
|
|
1415
2044
|
return dispatcher === undefined ? [] : [dispatcher];
|
|
1416
2045
|
});
|
|
1417
2046
|
},
|
|
2047
|
+
/**
|
|
2048
|
+
* Returns event dispatchers excluding System routes.
|
|
2049
|
+
*
|
|
2050
|
+
* @param dispatchers Event dispatchers to filter.
|
|
2051
|
+
* @returns Frozen domain event dispatchers.
|
|
2052
|
+
*/
|
|
1418
2053
|
domainEventDispatchers(dispatchers) {
|
|
1419
2054
|
return Object.freeze(dispatchers.filter((dispatcher) => !ContextParts.isSystemEventDispatcher(dispatcher)));
|
|
1420
2055
|
},
|
|
2056
|
+
/**
|
|
2057
|
+
* Returns System event dispatchers.
|
|
2058
|
+
*
|
|
2059
|
+
* @param dispatchers Event dispatchers to filter.
|
|
2060
|
+
* @returns Frozen System event dispatchers.
|
|
2061
|
+
*/
|
|
1421
2062
|
systemEventDispatchers(dispatchers) {
|
|
1422
2063
|
return Object.freeze(dispatchers.filter((dispatcher) => ContextParts.isSystemEventDispatcher(dispatcher)));
|
|
1423
2064
|
},
|
|
2065
|
+
/**
|
|
2066
|
+
* Checks a dispatcher's event family and rejects mixed families.
|
|
2067
|
+
*
|
|
2068
|
+
* @param dispatcher Dispatcher to inspect.
|
|
2069
|
+
* @returns True when its schemas are System event schemas.
|
|
2070
|
+
*/
|
|
1424
2071
|
isSystemEventDispatcher(dispatcher) {
|
|
1425
2072
|
const schemas = [...dispatcher.messageSchemas()];
|
|
1426
2073
|
const systemSchemas = schemas.filter((schema) => schema.typeName.startsWith("spine.system."));
|
|
@@ -1429,6 +2076,12 @@ const ContextParts = Object.freeze({
|
|
|
1429
2076
|
}
|
|
1430
2077
|
return systemSchemas.length > 0;
|
|
1431
2078
|
},
|
|
2079
|
+
/**
|
|
2080
|
+
* Collects repository-produced event schemas by type URL.
|
|
2081
|
+
*
|
|
2082
|
+
* @param repositories Repository views to inspect.
|
|
2083
|
+
* @returns Frozen unique event schemas.
|
|
2084
|
+
*/
|
|
1432
2085
|
repositoryProducedEventSchemas(repositories) {
|
|
1433
2086
|
const schemas = new Map();
|
|
1434
2087
|
for (const repository of repositories) {
|
|
@@ -1438,10 +2091,21 @@ const ContextParts = Object.freeze({
|
|
|
1438
2091
|
}
|
|
1439
2092
|
return Object.freeze([...schemas.values()]);
|
|
1440
2093
|
},
|
|
2094
|
+
/**
|
|
2095
|
+
* Clears metadata for a failed context build and closes its tenant index.
|
|
2096
|
+
*
|
|
2097
|
+
* @param context Context whose build failed.
|
|
2098
|
+
* @param tenantIndex Tenant index to close.
|
|
2099
|
+
*/
|
|
1441
2100
|
cleanupFailedContext(context, tenantIndex) {
|
|
1442
2101
|
ContextParts.clearContextMetadata(context);
|
|
1443
2102
|
tenantIndex.close();
|
|
1444
2103
|
},
|
|
2104
|
+
/**
|
|
2105
|
+
* Removes runtime metadata associated with a context.
|
|
2106
|
+
*
|
|
2107
|
+
* @param context Context whose WeakMap metadata is removed.
|
|
2108
|
+
*/
|
|
1445
2109
|
clearContextMetadata(context) {
|
|
1446
2110
|
const buses = contextEventBuses.get(context);
|
|
1447
2111
|
if (buses !== undefined) {
|
|
@@ -1451,7 +2115,8 @@ const ContextParts = Object.freeze({
|
|
|
1451
2115
|
const runtime = contextSubscriptionRuntimes.get(context);
|
|
1452
2116
|
if (runtime !== undefined)
|
|
1453
2117
|
subscriptionRuntimeAccess.clearLogger(runtime);
|
|
1454
|
-
|
|
2118
|
+
contextSignalPublishers.get(context)?.clearLogger();
|
|
2119
|
+
contextSignalPublishers.delete(context);
|
|
1455
2120
|
contextLoggers.delete(context);
|
|
1456
2121
|
contextSystemPairings.delete(context);
|
|
1457
2122
|
contextTenantIndexes.delete(context);
|
|
@@ -1462,48 +2127,72 @@ const ContextParts = Object.freeze({
|
|
|
1462
2127
|
eventSubscribers.delete(context);
|
|
1463
2128
|
systemEventPosters.delete(context);
|
|
1464
2129
|
},
|
|
2130
|
+
/**
|
|
2131
|
+
* Creates delivery operations from the context inboxes and readiness state.
|
|
2132
|
+
*
|
|
2133
|
+
* @param context Built context snapshot.
|
|
2134
|
+
* @param storageFactory Context storage provider.
|
|
2135
|
+
* @param tenantIndex Tenant index for startup scopes.
|
|
2136
|
+
* @param entityInbox Registered Entity inbox.
|
|
2137
|
+
* @param projections Projection inbox.
|
|
2138
|
+
* @param readiness Delivery readiness coordinator.
|
|
2139
|
+
* @returns Delivery descriptor.
|
|
2140
|
+
*/
|
|
1465
2141
|
createDeliveryDescriptor(context, storageFactory, tenantIndex, entityInbox, projections, readiness) {
|
|
1466
2142
|
return Object.freeze({
|
|
1467
2143
|
storageFactory,
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2144
|
+
startupScopes: () => ContextParts.deliveryStartupScopes(tenantIndex),
|
|
2145
|
+
storageContext: (scope) => ContextParts.deliveryStorageContext(context, scope),
|
|
2146
|
+
endpoints: () => Object.freeze([...entityInbox.endpoints(), ...projections.endpoints()]),
|
|
2147
|
+
replay: (message, tenantId) => message.label === "UPDATE_SUBSCRIBER"
|
|
2148
|
+
? projections.replay(message, tenantId)
|
|
2149
|
+
: entityInbox.replay(message, tenantId),
|
|
2150
|
+
onReady: (onReady) => readiness.onReady(onReady),
|
|
2151
|
+
transition: (scopes, onReady, options) => readiness.transition(scopes, onReady, options),
|
|
2152
|
+
});
|
|
2153
|
+
},
|
|
2154
|
+
/**
|
|
2155
|
+
* Lists delivery startup scopes, one per tenant in multitenant contexts.
|
|
2156
|
+
*
|
|
2157
|
+
* @param tenantIndex Tenant index supplying configured scopes.
|
|
2158
|
+
* @returns Frozen startup scope list.
|
|
2159
|
+
*/
|
|
2160
|
+
async deliveryStartupScopes(tenantIndex) {
|
|
2161
|
+
if (tenantIndex.tenantMode === "single-tenant") {
|
|
2162
|
+
return Object.freeze([Object.freeze({})]);
|
|
2163
|
+
}
|
|
2164
|
+
return Object.freeze((await tenantIndex.all()).map((tenantId) => Object.freeze({ tenantId })));
|
|
2165
|
+
},
|
|
2166
|
+
/**
|
|
2167
|
+
* Creates tenant-validated storage context for a delivery scope.
|
|
2168
|
+
*
|
|
2169
|
+
* @param context Built context snapshot.
|
|
2170
|
+
* @param scope Selected startup scope.
|
|
2171
|
+
* @returns Storage context for the selected tenant.
|
|
2172
|
+
*/
|
|
2173
|
+
deliveryStorageContext(context, scope) {
|
|
2174
|
+
if (context.tenantMode === "single-tenant") {
|
|
2175
|
+
if (scope.tenantId !== undefined) {
|
|
2176
|
+
throw new Error(`Single-tenant context "${context.name.value}" does not accept tenantId.`);
|
|
2177
|
+
}
|
|
2178
|
+
return Object.freeze({ name: context.name.value, multitenant: false });
|
|
2179
|
+
}
|
|
2180
|
+
const tenantId = scope.tenantId;
|
|
2181
|
+
if (tenantId === undefined) {
|
|
2182
|
+
throw new Error(`Multitenant context "${context.name.value}" requires tenantId.`);
|
|
2183
|
+
}
|
|
2184
|
+
return Object.freeze({
|
|
2185
|
+
name: context.name.value,
|
|
2186
|
+
multitenant: true,
|
|
2187
|
+
tenantId: TenantBoundary.from(tenantId).tenantId,
|
|
1505
2188
|
});
|
|
1506
2189
|
},
|
|
2190
|
+
/**
|
|
2191
|
+
* Closes prepared repositories and collects close failures.
|
|
2192
|
+
*
|
|
2193
|
+
* @param preparedRepositories Prepared repository resources to close.
|
|
2194
|
+
* @returns Collected close errors.
|
|
2195
|
+
*/
|
|
1507
2196
|
closePreparedRepositories(preparedRepositories) {
|
|
1508
2197
|
const errors = [];
|
|
1509
2198
|
for (const preparedRepository of preparedRepositories) {
|
|
@@ -1516,6 +2205,13 @@ const ContextParts = Object.freeze({
|
|
|
1516
2205
|
}
|
|
1517
2206
|
return errors;
|
|
1518
2207
|
},
|
|
2208
|
+
/**
|
|
2209
|
+
* Calls one asynchronous context close action and records failures.
|
|
2210
|
+
*
|
|
2211
|
+
* @param close Close operation.
|
|
2212
|
+
* @param errors Failure collection.
|
|
2213
|
+
* @returns Promise fulfilled after the close attempt is recorded.
|
|
2214
|
+
*/
|
|
1519
2215
|
async closeContextPart(close, errors) {
|
|
1520
2216
|
try {
|
|
1521
2217
|
await close();
|
|
@@ -1524,17 +2220,37 @@ const ContextParts = Object.freeze({
|
|
|
1524
2220
|
ContextParts.collectCloseError(error, errors);
|
|
1525
2221
|
}
|
|
1526
2222
|
},
|
|
1527
|
-
|
|
2223
|
+
/**
|
|
2224
|
+
* Waits until accepted command, event, System event, and signal work drains.
|
|
2225
|
+
*
|
|
2226
|
+
* @param commandBus Command bus.
|
|
2227
|
+
* @param eventBus Domain event bus.
|
|
2228
|
+
* @param systemEventBus System event bus.
|
|
2229
|
+
* @param publisher Signal publisher.
|
|
2230
|
+
* @returns Promise fulfilled when accepted work stabilizes.
|
|
2231
|
+
*/
|
|
2232
|
+
async drainContextWork(commandBus, eventBus, systemEventBus, publisher) {
|
|
1528
2233
|
let observedCommandWork = -1;
|
|
1529
2234
|
let observedEventWork = -1;
|
|
2235
|
+
let observedSystemEventWork = -1;
|
|
1530
2236
|
do {
|
|
1531
2237
|
observedCommandWork = commandBusAccess.acceptedWorkCount(commandBus);
|
|
1532
2238
|
observedEventWork = eventBusAccess.acceptedWorkCount(eventBus);
|
|
2239
|
+
observedSystemEventWork = eventBusAccess.acceptedWorkCount(systemEventBus);
|
|
1533
2240
|
await commandBusAccess.drain(commandBus);
|
|
1534
2241
|
await eventBusAccess.drain(eventBus);
|
|
2242
|
+
await eventBusAccess.drain(systemEventBus);
|
|
2243
|
+
await publisher.drain();
|
|
1535
2244
|
} while (commandBusAccess.acceptedWorkCount(commandBus) !== observedCommandWork ||
|
|
1536
|
-
eventBusAccess.acceptedWorkCount(eventBus) !== observedEventWork
|
|
2245
|
+
eventBusAccess.acceptedWorkCount(eventBus) !== observedEventWork ||
|
|
2246
|
+
eventBusAccess.acceptedWorkCount(systemEventBus) !== observedSystemEventWork);
|
|
1537
2247
|
},
|
|
2248
|
+
/**
|
|
2249
|
+
* Adds an error or aggregate causes to the close error collection.
|
|
2250
|
+
*
|
|
2251
|
+
* @param error Close failure.
|
|
2252
|
+
* @param errors Failure collection.
|
|
2253
|
+
*/
|
|
1538
2254
|
collectCloseError(error, errors) {
|
|
1539
2255
|
if (error instanceof AggregateError) {
|
|
1540
2256
|
const causes = error.errors;
|
|
@@ -1545,6 +2261,13 @@ const ContextParts = Object.freeze({
|
|
|
1545
2261
|
}
|
|
1546
2262
|
errors.push(error);
|
|
1547
2263
|
},
|
|
2264
|
+
/**
|
|
2265
|
+
* Binds a repository to context runtime services and supplies commit and close operations.
|
|
2266
|
+
*
|
|
2267
|
+
* @param repository Repository to prepare.
|
|
2268
|
+
* @param registration Context services and schemas.
|
|
2269
|
+
* @returns Prepared repository.
|
|
2270
|
+
*/
|
|
1548
2271
|
prepareRepositoryForContext(repository, registration) {
|
|
1549
2272
|
ContextParts.requireRepositoryInstance(repository, "BoundedContext repository registration");
|
|
1550
2273
|
const snapshot = ContextParts.repositorySnapshot(repository);
|
|
@@ -1556,14 +2279,9 @@ const ContextParts = Object.freeze({
|
|
|
1556
2279
|
signalMetadata: new SignalMetadata(),
|
|
1557
2280
|
entityInbox: registration.entityInbox,
|
|
1558
2281
|
projectionInbox: registration.projectionInbox,
|
|
1559
|
-
|
|
1560
|
-
dispatchStoredFollowUp: registration.dispatchStoredFollowUp,
|
|
1561
|
-
postEventFollowUp: registration.postEventFollowUp,
|
|
2282
|
+
publisher: registration.publisher,
|
|
1562
2283
|
registerEventSchema: registration.registerEventSchema,
|
|
1563
2284
|
registerSystemEventSchema: registration.registerSystemEventSchema,
|
|
1564
|
-
postSystemFollowUp: registration.postSystemFollowUp,
|
|
1565
|
-
onPostCommand: registration.onPostCommand,
|
|
1566
|
-
recordDispatchFailure: registration.recordDispatchFailure,
|
|
1567
2285
|
});
|
|
1568
2286
|
const entityInboxTarget = repositoryAccess.entityInboxTarget(repository);
|
|
1569
2287
|
const projectionInboxTarget = repositoryAccess.projectionInboxTarget(repository);
|
|
@@ -1580,6 +2298,12 @@ const ContextParts = Object.freeze({
|
|
|
1580
2298
|
},
|
|
1581
2299
|
};
|
|
1582
2300
|
},
|
|
2301
|
+
/**
|
|
2302
|
+
* Captures repository metadata needed for context registration.
|
|
2303
|
+
*
|
|
2304
|
+
* @param repository Repository to inspect.
|
|
2305
|
+
* @returns Frozen registration snapshot.
|
|
2306
|
+
*/
|
|
1583
2307
|
repositorySnapshot(repository) {
|
|
1584
2308
|
const snapshot = repositoryAccess.snapshot(repository);
|
|
1585
2309
|
return Object.freeze({
|
|
@@ -1592,6 +2316,11 @@ const ContextParts = Object.freeze({
|
|
|
1592
2316
|
snapshot,
|
|
1593
2317
|
});
|
|
1594
2318
|
},
|
|
2319
|
+
/**
|
|
2320
|
+
* Rejects a repository already registered with a bounded context.
|
|
2321
|
+
*
|
|
2322
|
+
* @param repository Repository to check.
|
|
2323
|
+
*/
|
|
1595
2324
|
rejectRegisteredRepository(repository) {
|
|
1596
2325
|
const snapshot = ContextParts.repositorySnapshot(repository);
|
|
1597
2326
|
const registration = registeredRepositories.get(repository);
|
|
@@ -1600,9 +2329,21 @@ const ContextParts = Object.freeze({
|
|
|
1600
2329
|
`"${registration.name.value}".`);
|
|
1601
2330
|
}
|
|
1602
2331
|
},
|
|
2332
|
+
/**
|
|
2333
|
+
* Creates storage record columns from Entity metadata.
|
|
2334
|
+
*
|
|
2335
|
+
* @param snapshot Registration snapshot supplying fields.
|
|
2336
|
+
* @returns Record columns.
|
|
2337
|
+
*/
|
|
1603
2338
|
repositoryColumns(snapshot) {
|
|
1604
2339
|
return snapshot.metadata.columns.map((field) => new RecordColumn(field.name, ColumnTypes.fromField(field.descriptor), (record) => ContextParts.readRecordField(record, field.localName)));
|
|
1605
2340
|
},
|
|
2341
|
+
/**
|
|
2342
|
+
* Creates an immutable repository view from a registration snapshot.
|
|
2343
|
+
*
|
|
2344
|
+
* @param snapshot Registration snapshot.
|
|
2345
|
+
* @returns Immutable repository view.
|
|
2346
|
+
*/
|
|
1606
2347
|
createRepositoryView(snapshot) {
|
|
1607
2348
|
return Object.freeze({
|
|
1608
2349
|
entityType: snapshot.entityType,
|
|
@@ -1614,35 +2355,12 @@ const ContextParts = Object.freeze({
|
|
|
1614
2355
|
snapshot: snapshot.snapshot,
|
|
1615
2356
|
});
|
|
1616
2357
|
},
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
snapshotDispatchError(error) {
|
|
1624
|
-
if (error instanceof Error) {
|
|
1625
|
-
const snapshot = {
|
|
1626
|
-
name: ContextParts.boundedErrorString(error.name, dispatchErrorMessageLimit) || "Error",
|
|
1627
|
-
message: ContextParts.boundedErrorString(error.message, dispatchErrorMessageLimit),
|
|
1628
|
-
...(typeof error.stack === "string"
|
|
1629
|
-
? { stack: ContextParts.boundedErrorString(error.stack, dispatchErrorStackLimit) }
|
|
1630
|
-
: {}),
|
|
1631
|
-
};
|
|
1632
|
-
return Object.freeze(snapshot);
|
|
1633
|
-
}
|
|
1634
|
-
return Object.freeze({
|
|
1635
|
-
name: "NonErrorThrow",
|
|
1636
|
-
message: ContextParts.boundedErrorString(String(error), dispatchErrorMessageLimit),
|
|
1637
|
-
});
|
|
1638
|
-
},
|
|
1639
|
-
cloneDispatchError(error) {
|
|
1640
|
-
return Object.freeze({
|
|
1641
|
-
name: error.name,
|
|
1642
|
-
message: error.message,
|
|
1643
|
-
...(error.stack === undefined ? {} : { stack: error.stack }),
|
|
1644
|
-
});
|
|
1645
|
-
},
|
|
2358
|
+
/**
|
|
2359
|
+
* Builds dispatch descriptors for Projection repositories with event handlers.
|
|
2360
|
+
*
|
|
2361
|
+
* @param repositories Repository views to inspect.
|
|
2362
|
+
* @returns Frozen Projection dispatch descriptors.
|
|
2363
|
+
*/
|
|
1646
2364
|
projectionDispatchers(repositories) {
|
|
1647
2365
|
const projections = [];
|
|
1648
2366
|
for (const repository of repositories) {
|
|
@@ -1661,6 +2379,12 @@ const ContextParts = Object.freeze({
|
|
|
1661
2379
|
}
|
|
1662
2380
|
return Object.freeze(projections);
|
|
1663
2381
|
},
|
|
2382
|
+
/**
|
|
2383
|
+
* Collects unique Projection state schemas for clearing.
|
|
2384
|
+
*
|
|
2385
|
+
* @param projections Projection dispatch descriptors.
|
|
2386
|
+
* @returns Frozen unique clear targets.
|
|
2387
|
+
*/
|
|
1664
2388
|
projectionStateClearTargets(projections) {
|
|
1665
2389
|
const unique = new Map();
|
|
1666
2390
|
for (const projection of projections) {
|
|
@@ -1673,6 +2397,13 @@ const ContextParts = Object.freeze({
|
|
|
1673
2397
|
}
|
|
1674
2398
|
return Object.freeze([...unique.values()]);
|
|
1675
2399
|
},
|
|
2400
|
+
/**
|
|
2401
|
+
* Reads stored events in deterministic timestamp, producer, version, and ID order.
|
|
2402
|
+
*
|
|
2403
|
+
* @param context Event-store storage context.
|
|
2404
|
+
* @param storageFactory Event storage provider.
|
|
2405
|
+
* @returns Stored events in deterministic order.
|
|
2406
|
+
*/
|
|
1676
2407
|
async readStoredEvents(context, storageFactory) {
|
|
1677
2408
|
const eventStore = new EventStore(context, storageFactory);
|
|
1678
2409
|
try {
|
|
@@ -1689,12 +2420,24 @@ const ContextParts = Object.freeze({
|
|
|
1689
2420
|
eventStore.close();
|
|
1690
2421
|
}
|
|
1691
2422
|
},
|
|
2423
|
+
/**
|
|
2424
|
+
* Builds Stand options for the catch-up tenant scope.
|
|
2425
|
+
*
|
|
2426
|
+
* @param context Catch-up storage context.
|
|
2427
|
+
* @returns Options with a cloned tenant ID when multitenant.
|
|
2428
|
+
*/
|
|
1692
2429
|
catchUpStandOptions(context) {
|
|
1693
2430
|
if (!context.multitenant) {
|
|
1694
2431
|
return {};
|
|
1695
2432
|
}
|
|
1696
2433
|
return Object.freeze({ tenantId: clone(TenantIdSchema, context.tenantId) });
|
|
1697
2434
|
},
|
|
2435
|
+
/**
|
|
2436
|
+
* Verifies a stored event envelope belongs to the catch-up tenant.
|
|
2437
|
+
*
|
|
2438
|
+
* @param context Catch-up storage context.
|
|
2439
|
+
* @param event Stored event to validate.
|
|
2440
|
+
*/
|
|
1698
2441
|
validateReplayTenant(context, event) {
|
|
1699
2442
|
if (!context.multitenant) {
|
|
1700
2443
|
return;
|
|
@@ -1708,6 +2451,12 @@ const ContextParts = Object.freeze({
|
|
|
1708
2451
|
throw new Error("Read-side catch-up stored event envelope tenant does not match.");
|
|
1709
2452
|
}
|
|
1710
2453
|
},
|
|
2454
|
+
/**
|
|
2455
|
+
* Reads the tenant ID from an event import or past-message origin.
|
|
2456
|
+
*
|
|
2457
|
+
* @param event Event whose origin is inspected.
|
|
2458
|
+
* @returns Cloned tenant ID when present.
|
|
2459
|
+
*/
|
|
1711
2460
|
readReplayTenant(event) {
|
|
1712
2461
|
switch (event.context?.origin.case) {
|
|
1713
2462
|
case "importContext":
|
|
@@ -1718,9 +2467,23 @@ const ContextParts = Object.freeze({
|
|
|
1718
2467
|
return undefined;
|
|
1719
2468
|
}
|
|
1720
2469
|
},
|
|
2470
|
+
/**
|
|
2471
|
+
* Copies a tenant ID when present.
|
|
2472
|
+
*
|
|
2473
|
+
* @param tenantId Optional tenant ID.
|
|
2474
|
+
* @returns Cloned tenant ID or undefined.
|
|
2475
|
+
*/
|
|
1721
2476
|
tenantIdValue(tenantId) {
|
|
1722
2477
|
return tenantId === undefined ? undefined : clone(TenantIdSchema, tenantId);
|
|
1723
2478
|
},
|
|
2479
|
+
/**
|
|
2480
|
+
* Dispatches a stored event to matching Projection handlers and direct updates.
|
|
2481
|
+
*
|
|
2482
|
+
* @param projections Projection dispatch descriptors.
|
|
2483
|
+
* @param event Stored event.
|
|
2484
|
+
* @param rebuild Whether direct dispatch is a rebuild.
|
|
2485
|
+
* @returns One when a Projection matched, otherwise zero.
|
|
2486
|
+
*/
|
|
1724
2487
|
async dispatchStoredProjectionEvent(projections, event, rebuild) {
|
|
1725
2488
|
const typeUrl = event.message?.typeUrl;
|
|
1726
2489
|
if (typeUrl === undefined || typeUrl === "") {
|
|
@@ -1735,24 +2498,51 @@ const ContextParts = Object.freeze({
|
|
|
1735
2498
|
}
|
|
1736
2499
|
return matching.length > 0 ? 1 : 0;
|
|
1737
2500
|
},
|
|
2501
|
+
/**
|
|
2502
|
+
* Creates a replay error with the event ID and bounded cause details.
|
|
2503
|
+
*
|
|
2504
|
+
* @param event Event that failed.
|
|
2505
|
+
* @param cause Thrown replay cause.
|
|
2506
|
+
* @returns Replay error.
|
|
2507
|
+
*/
|
|
1738
2508
|
catchUpReplayError(event, cause) {
|
|
1739
2509
|
return new CatchUpReplayError(event.id?.value ?? "(missing)", ContextParts.catchUpReplayDetail(cause));
|
|
1740
2510
|
},
|
|
2511
|
+
/**
|
|
2512
|
+
* Returns an error string truncated to a maximum length.
|
|
2513
|
+
*
|
|
2514
|
+
* @param value String to bound.
|
|
2515
|
+
* @param limit Maximum character count.
|
|
2516
|
+
* @returns Bounded string.
|
|
2517
|
+
*/
|
|
1741
2518
|
boundedErrorString(value, limit) {
|
|
1742
2519
|
return value.length <= limit ? value : `${value.slice(0, limit - 3)}...`;
|
|
1743
2520
|
},
|
|
2521
|
+
/**
|
|
2522
|
+
* Converts a thrown value to bounded error name and message details.
|
|
2523
|
+
*
|
|
2524
|
+
* @param error Thrown value.
|
|
2525
|
+
* @returns Immutable bounded details.
|
|
2526
|
+
*/
|
|
1744
2527
|
catchUpReplayDetail(error) {
|
|
1745
2528
|
if (error instanceof Error) {
|
|
1746
2529
|
return Object.freeze({
|
|
1747
|
-
name: ContextParts.boundedErrorString(error.name,
|
|
1748
|
-
message: ContextParts.boundedErrorString(error.message,
|
|
2530
|
+
name: ContextParts.boundedErrorString(error.name, errorDetailLimit) || "Error",
|
|
2531
|
+
message: ContextParts.boundedErrorString(error.message, errorDetailLimit),
|
|
1749
2532
|
});
|
|
1750
2533
|
}
|
|
1751
2534
|
return Object.freeze({
|
|
1752
2535
|
name: "NonErrorThrow",
|
|
1753
|
-
message: ContextParts.boundedErrorString(String(error),
|
|
2536
|
+
message: ContextParts.boundedErrorString(String(error), errorDetailLimit),
|
|
1754
2537
|
});
|
|
1755
2538
|
},
|
|
2539
|
+
/**
|
|
2540
|
+
* Reads and requires the Entity ID field from a stored record.
|
|
2541
|
+
*
|
|
2542
|
+
* @param record Protobuf record.
|
|
2543
|
+
* @param snapshot Repository metadata identifying the ID field.
|
|
2544
|
+
* @returns ID field value; throws when missing.
|
|
2545
|
+
*/
|
|
1756
2546
|
readRecordId(record, snapshot) {
|
|
1757
2547
|
const value = ContextParts.readRecordField(record, snapshot.idField.localName);
|
|
1758
2548
|
if (value === undefined || value === null) {
|
|
@@ -1760,6 +2550,13 @@ const ContextParts = Object.freeze({
|
|
|
1760
2550
|
}
|
|
1761
2551
|
return value;
|
|
1762
2552
|
},
|
|
2553
|
+
/**
|
|
2554
|
+
* Reads a field from a Protobuf record by descriptor local name.
|
|
2555
|
+
*
|
|
2556
|
+
* @param record Record message.
|
|
2557
|
+
* @param localName Descriptor local field name.
|
|
2558
|
+
* @returns Field value.
|
|
2559
|
+
*/
|
|
1763
2560
|
readRecordField(record, localName) {
|
|
1764
2561
|
return record[localName];
|
|
1765
2562
|
},
|