@spine-event-engine/server 2.0.0-snapshot.15 → 2.0.0-snapshot.16

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.
@@ -47,7 +47,7 @@ import { TransitionValidationError } from "./command-errors.js";
47
47
  import { MessageIds, PrimitiveIds } from "./primitive-id.js";
48
48
  import { ImplicitRequiredIds } from "../entity/implicit-required-id.js";
49
49
  /**
50
- * Describes an error raised when repository identity metadata cannot be constructed.
50
+ * Error raised when an Entity class and state schema cannot form a repository.
51
51
  */
52
52
  export class RepositoryIdentityError extends Error {
53
53
  // prettier-ignore
@@ -69,30 +69,17 @@ export class RepositoryIdentityError extends Error {
69
69
  }
70
70
  }
71
71
  /**
72
- * Repository identity and context-owned storage registration over one entity constructor and state schema.
72
+ * Configures storage and handlers for one Entity class.
73
73
  *
74
- * The class records the ownership facts bounded-context registration needs for
75
- * duplicate and conflict checks. Context assembly uses this metadata to attach
76
- * a repository to one built context and open state record storage. With
77
- * authentic explicit aggregate handler metadata, the built context can also
78
- * execute aggregate commands through repository-owned assignees and live
79
- * direct transactional handlers, persist the latest aggregate state plus a diagnostic event journal,
80
- * and hand already-stored events to the event bus. Aggregate and process-manager command
81
- * execution require `command.id` before routing or mutation so produced events
82
- * can carry a contract-valid command origin. With authentic projection
83
- * subscriber metadata, built contexts can also execute projection subscribers
84
- * and write changed projection state through the context-owned `Stand`. With
85
- * authentic process-manager metadata, built contexts can execute command
86
- * assignees, event reactors, and event-commanding handlers, storing changed
87
- * process-manager state through tenant-scoped Stand records with full Spine
88
- * Versions. The `events` option declares generated event schemas emitted by
89
- * aggregate and process-manager producer handlers. The repository surface still
90
- * does not expose direct entity lookup/storage APIs, inbox/delivery management,
91
- * caches, lifecycle monitors, or transport startup.
74
+ * Construction selects the Entity class, state schema, routing, and optional
75
+ * creation callback. A Bounded Context builder registers the repository. During
76
+ * context assembly, it opens record storage and installs handlers and Inbox
77
+ * targets. Aggregate handlers store state and a diagnostic event journal;
78
+ * Projection and Process Manager handlers store state in Stand. The `events`
79
+ * option lists generated event schemas that handlers may emit.
92
80
  *
93
- * @typeParam EntityType - A single concrete aggregate, projection, or process-manager constructor
94
- * with one concrete generated state schema. Broad constructor, constructor-union, broad-schema, and
95
- * schema-union bindings intentionally fail the public type constraint.
81
+ * @typeParam EntityType One concrete Aggregate, Projection, or Process Manager
82
+ * class with one generated state schema.
96
83
  */
97
84
  export class Repository {
98
85
  #entityType;
@@ -105,53 +92,18 @@ export class Repository {
105
92
  * @param options The entity constructor, state schema, and optional routing configuration.
106
93
  */
107
94
  constructor(options) {
108
- if (!RepositoryIdentity.isRepositoryOptionsObject(options)) {
109
- throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "Repository options must be a non-null object with an entity type class constructor " +
110
- "extending Aggregate, Projection, or ProcessManager.");
111
- }
112
- const entityType = RepositoryIdentity.readEntityTypeOption(options);
113
- const entityTypeDisplayName = RepositoryIdentity.entityTypeName(entityType);
114
- if (typeof entityType !== "function" || !RepositoryIdentity.isClassConstructor(entityType)) {
115
- throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", `Repository entity type "${entityTypeDisplayName}" must be a class constructor ` +
116
- "extending Aggregate, Projection, or ProcessManager.");
117
- }
118
- const entityFamily = RepositoryIdentity.resolveRepositoryEntityFamily(entityType);
119
- if (entityFamily === undefined) {
120
- throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", `Repository entity type "${entityTypeDisplayName}" must extend Aggregate, Projection, or ProcessManager.`);
121
- }
122
- const schema = RepositoryIdentity.readRepositorySchemaOption(options, entityTypeDisplayName, entityFamily);
123
- const metadata = RepositoryIdentity.describeRepositoryEntityMetadata(entityTypeDisplayName, entityFamily, schema);
124
- if (metadata.kind !== entityFamily) {
125
- throw new RepositoryIdentityError("ENTITY_SCHEMA_KIND_MISMATCH", `Repository entity type "${entityTypeDisplayName}" does not match ` +
126
- "the supplied state schema.");
127
- }
128
- if (options.stateUpdateRouting !== undefined && entityFamily !== "projection") {
129
- throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "State-update routing is supported only by Projection repositories.");
130
- }
95
+ const { entityType, entityFamily, schema, metadata } = RepositoryIdentity.describeOptions(options);
131
96
  attachEntitySchema(entityType, schema);
132
97
  this.#entityType = entityType;
133
98
  this.#entityFamily = entityFamily;
134
99
  this.#metadata = metadata;
135
100
  this.#routing = RepositoryRoutes.createRepositoryRouting(this.#entityType, this.#entityFamily, this.#metadata, options.handlers, options.events ?? [], CommandRoutingInternals.snapshot(options.commandRouting), EventRoutingInternals.snapshot(options.eventRouting), StateUpdateRoutingInternals.snapshot(options.stateUpdateRouting), new StringifierRegistry(options.stringifierRegistry));
136
- repositoryRoutings.set(this, this.#routing);
137
- repositoryProducedEventSchemas.set(this, Object.freeze([...this.#routing.producedEventSchemas]));
138
- repositoryHistoryConfigurations.set(this, RepositoryStorage.readHistoryConfiguration(options, this.#entityFamily));
139
- repositorySnapshots.set(this, RepositoryIdentity.createRepositorySnapshot(this.#entityType, this.#entityFamily, this.#metadata));
140
- repositoryDispatchers.set(this, RepositoryDispatch.createRepositoryDispatchers(this, this.#routing));
141
- const entityInboxTarget = RepositoryDispatch.createEntityInboxTarget(this, this.#routing);
142
- const projectionInboxTarget = RepositoryDispatch.createProjectionInboxTarget(this, this.#routing);
143
- if (entityInboxTarget !== undefined) {
144
- repositoryEntityInboxTargets.set(this, entityInboxTarget);
145
- }
146
- if (projectionInboxTarget !== undefined) {
147
- repositoryProjectionInboxTargets.set(this, projectionInboxTarget);
148
- repositoryProjectionDirect.set(this, RepositoryDispatch.createProjectionDirectDispatch(this, this.#routing));
149
- }
101
+ RepositoryRegistration.install(this, options, this.#routing, entityType, entityFamily, metadata);
150
102
  }
151
103
  /**
152
- * Returns the entity constructor owned by this repository identity.
104
+ * Returns the Entity class registered with this repository.
153
105
  *
154
- * @returns The owned entity constructor.
106
+ * @returns The registered Entity class.
155
107
  */
156
108
  get entityType() {
157
109
  return this.#entityType;
@@ -167,7 +119,7 @@ export class Repository {
167
119
  /**
168
120
  * Returns the generated schema for this repository's entity state.
169
121
  *
170
- * @returns The owned state schema.
122
+ * @returns The Entity state schema.
171
123
  */
172
124
  get stateSchema() {
173
125
  return this.#metadata.schema;
@@ -196,15 +148,15 @@ export class Repository {
196
148
  }
197
149
  }
198
150
  /**
199
- * Returns descriptor-derived metadata for the owned entity state.
151
+ * Returns descriptor metadata for this repository's Entity state.
200
152
  *
201
- * @returns A metadata view for the owned state.
153
+ * @returns Descriptor metadata for the Entity state.
202
154
  */
203
155
  get metadata() {
204
156
  return this.#metadata;
205
157
  }
206
158
  /**
207
- * Returns the fully qualified Protobuf name of the owned entity state.
159
+ * Returns the fully qualified Protobuf name of the Entity state.
208
160
  *
209
161
  * @returns The state message type name.
210
162
  */
@@ -253,6 +205,7 @@ export class Repository {
253
205
  }
254
206
  }
255
207
  const repositorySnapshots = new WeakMap();
208
+ const repositoryCreators = new WeakMap();
256
209
  const repositoryRoutings = new WeakMap();
257
210
  const repositoryProducedEventSchemas = new WeakMap();
258
211
  const repositoryDispatchers = new WeakMap();
@@ -264,6 +217,50 @@ const repositoryEntityHandles = new WeakMap();
264
217
  const entityStateHistoryCaches = new WeakMap();
265
218
  const repositoryHistoryConfigurations = new WeakMap();
266
219
  const repositoryDispatchGuards = new WeakMap();
220
+ /**
221
+ * Installs repository routing, history, and callback state after identity validation.
222
+ */
223
+ const RepositoryRegistration = {
224
+ /**
225
+ * Registers framework dispatch and application construction for one repository.
226
+ *
227
+ * @typeParam EntityType Concrete Entity class registered by the repository.
228
+ * @param repository Validated repository instance.
229
+ * @param options Registration options captured at construction.
230
+ * @param routing Resolved handler routes.
231
+ * @param entityType Validated Entity constructor.
232
+ * @param entityFamily Validated Entity family.
233
+ * @param metadata Validated descriptor metadata.
234
+ */
235
+ install(repository, options, routing, entityType, entityFamily, metadata) {
236
+ if (options.onCreate !== undefined) {
237
+ repositoryCreators.set(repository, options.onCreate);
238
+ }
239
+ repositoryRoutings.set(repository, routing);
240
+ repositoryProducedEventSchemas.set(repository, Object.freeze([...routing.producedEventSchemas]));
241
+ repositoryHistoryConfigurations.set(repository, RepositoryStorage.readHistoryConfiguration(options, entityFamily));
242
+ repositorySnapshots.set(repository, RepositoryIdentity.createRepositorySnapshot(entityType, entityFamily, metadata));
243
+ repositoryDispatchers.set(repository, RepositoryDispatch.createRepositoryDispatchers(repository, routing));
244
+ RepositoryRegistration.installTargets(repository, routing);
245
+ },
246
+ /**
247
+ * Registers durable Entity and direct Projection delivery targets.
248
+ *
249
+ * @param repository Repository receiving delivery targets.
250
+ * @param routing Resolved handler routes.
251
+ */
252
+ installTargets(repository, routing) {
253
+ const entityTarget = RepositoryDispatch.createEntityInboxTarget(repository, routing);
254
+ const projectionTarget = RepositoryDispatch.createProjectionInboxTarget(repository, routing);
255
+ if (entityTarget !== undefined)
256
+ repositoryEntityInboxTargets.set(repository, entityTarget);
257
+ if (projectionTarget === undefined)
258
+ return;
259
+ repositoryProjectionInboxTargets.set(repository, projectionTarget);
260
+ repositoryProjectionDirect.set(repository, RepositoryDispatch.createProjectionDirectDispatch(repository, routing));
261
+ },
262
+ };
263
+ Object.freeze(RepositoryRegistration);
267
264
  Object.freeze(Repository);
268
265
  /**
269
266
  * Exposes framework-only repository access for bounded-context assembly.
@@ -272,7 +269,7 @@ Object.freeze(Repository);
272
269
  */
273
270
  export const repositoryAccess = Object.freeze({
274
271
  /**
275
- * Checks whether a value has registered repository identity metadata.
272
+ * Checks whether a value is a registered Repository.
276
273
  *
277
274
  * @param repository Value to inspect.
278
275
  * @returns Whether the value is a repository view.
@@ -281,7 +278,7 @@ export const repositoryAccess = Object.freeze({
281
278
  return repositorySnapshots.has(repository);
282
279
  },
283
280
  /**
284
- * Copies a repository's immutable identity metadata.
281
+ * Copies the Entity class, state schema, and ID details of a Repository.
285
282
  *
286
283
  * @param repository Repository to inspect.
287
284
  * @returns A copy-safe identity snapshot.
@@ -419,6 +416,26 @@ export const repositoryAccess = Object.freeze({
419
416
  },
420
417
  });
421
418
  const inboxDedupMs = 30_000;
419
+ /**
420
+ * Creates a repository Entity from framework options and an optional application constructor callback.
421
+ */
422
+ const RepositoryCreation = {
423
+ /**
424
+ * Creates one Entity and rejects wrong-class or Promise results before handler execution.
425
+ *
426
+ * @param repository The registered constructor and callback.
427
+ * @param options Framework values for this new or restored instance.
428
+ * @returns The registered Entity instance.
429
+ */
430
+ create(repository, options) {
431
+ const onCreate = repositoryCreators.get(repository);
432
+ const entityType = repository.entityType;
433
+ const entity = onCreate === undefined ? new entityType(options) : onCreate(options);
434
+ if (entity instanceof entityType)
435
+ return entity;
436
+ throw new TypeError(`Repository onCreate must synchronously return a ${entityType.name} instance.`);
437
+ },
438
+ };
422
439
  /**
423
440
  * Loads and persists Aggregate state within one tenant storage context.
424
441
  */
@@ -598,7 +615,6 @@ class AggregateExecutionSupport {
598
615
  * @returns The constructed Aggregate instance.
599
616
  */
600
617
  #instantiateAggregate(entityId, current) {
601
- const entityType = this.#repository.entityType;
602
618
  const options = {
603
619
  id: entityId,
604
620
  schema: this.#repository.stateSchema,
@@ -608,7 +624,7 @@ class AggregateExecutionSupport {
608
624
  if (current !== undefined) {
609
625
  options.lifecycle = { archived: current.archived, deleted: current.deleted };
610
626
  }
611
- return new entityType(options);
627
+ return RepositoryCreation.create(this.#repository, options);
612
628
  }
613
629
  /**
614
630
  * Creates generated initial state with its canonical identifier field set.
@@ -735,7 +751,7 @@ class AggregateCommandExecution {
735
751
  });
736
752
  }
737
753
  /**
738
- * Invokes an assignee in a fenced Entity transaction with output validation.
754
+ * Invokes an assignee, validates its output, and checks delivery before the Entity transaction commits.
739
755
  *
740
756
  * @param entity Aggregate instance to mutate.
741
757
  * @param handler Assignee declaration to invoke.
@@ -966,11 +982,11 @@ class AggregateEventExecution {
966
982
  }
967
983
  }
968
984
  /**
969
- * Applies a fenced Entity commit and raises transition rejection.
985
+ * Checks delivery immediately before the Entity transaction commits and raises transition rejection.
970
986
  *
971
987
  * @param entity Aggregate instance to commit.
972
988
  * @param producedEvents Whether reactors produced Events.
973
- * @returns Completion after a successful fenced commit.
989
+ * @returns Completion after a successful commit.
974
990
  */
975
991
  async #commitEntity(entity, producedEvents) {
976
992
  const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current, producedEvents));
@@ -1662,7 +1678,7 @@ class ProcessManagerCommandExecution {
1662
1678
  }
1663
1679
  }
1664
1680
  /**
1665
- * Invokes the Command handler in a fenced Entity transaction.
1681
+ * Invokes a Command handler and checks delivery before the Entity transaction commits.
1666
1682
  *
1667
1683
  * @param entity Process Manager instance to mutate.
1668
1684
  * @param assignee Registered Command handler.
@@ -1968,11 +1984,11 @@ class ProcessManagerEventExecution {
1968
1984
  }
1969
1985
  }
1970
1986
  /**
1971
- * Applies a fenced Entity commit and raises transition rejection.
1987
+ * Checks delivery immediately before the Entity transaction commits and raises transition rejection.
1972
1988
  *
1973
1989
  * @param entity Process Manager instance to commit.
1974
1990
  * @param producedEvents Whether handlers produced Events.
1975
- * @returns Completion after a successful fenced commit.
1991
+ * @returns Completion after a successful commit.
1976
1992
  */
1977
1993
  async #commitEntity(entity, producedEvents) {
1978
1994
  const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current, producedEvents));
@@ -2061,6 +2077,58 @@ class ProcessManagerEventExecution {
2061
2077
  * Internal repository identity operations.
2062
2078
  */
2063
2079
  const RepositoryIdentity = {
2080
+ /**
2081
+ * Validates constructor, family, and state metadata from repository options.
2082
+ *
2083
+ * @typeParam EntityType Concrete Entity constructor being registered.
2084
+ * @param options Repository configuration to validate.
2085
+ * @returns The validated Entity identity and descriptor metadata.
2086
+ */
2087
+ describeOptions(options) {
2088
+ const entityType = RepositoryIdentity.requireEntityType(options);
2089
+ const entityFamily = RepositoryIdentity.requireEntityFamily(entityType);
2090
+ const displayName = RepositoryIdentity.entityTypeName(entityType);
2091
+ const schema = RepositoryIdentity.readRepositorySchemaOption(options, displayName, entityFamily);
2092
+ const metadata = RepositoryIdentity.describeRepositoryEntityMetadata(displayName, entityFamily, schema);
2093
+ if (metadata.kind !== entityFamily) {
2094
+ throw new RepositoryIdentityError("ENTITY_SCHEMA_KIND_MISMATCH", `Repository entity type "${displayName}" does not match the supplied state schema.`);
2095
+ }
2096
+ if (options.stateUpdateRouting !== undefined && entityFamily !== "projection") {
2097
+ throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "State-update routing is supported only by Projection repositories.");
2098
+ }
2099
+ return { entityType, entityFamily, schema, metadata };
2100
+ },
2101
+ /**
2102
+ * Validates and returns a readable Entity class constructor in repository options.
2103
+ *
2104
+ * @param options Repository configuration to inspect.
2105
+ * @returns The supplied Entity constructor.
2106
+ */
2107
+ requireEntityType(options) {
2108
+ if (!RepositoryIdentity.isRepositoryOptionsObject(options)) {
2109
+ throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", "Repository options must be a non-null object with an entity type class constructor " +
2110
+ "extending Aggregate, Projection, or ProcessManager.");
2111
+ }
2112
+ const entityType = RepositoryIdentity.readEntityTypeOption(options);
2113
+ if (typeof entityType !== "function" || !RepositoryIdentity.isClassConstructor(entityType)) {
2114
+ throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", `Repository entity type "${RepositoryIdentity.entityTypeName(entityType)}" must be a class constructor ` +
2115
+ "extending Aggregate, Projection, or ProcessManager.");
2116
+ }
2117
+ return entityType;
2118
+ },
2119
+ /**
2120
+ * Validates and returns Aggregate, Projection, or Process Manager inheritance.
2121
+ *
2122
+ * @param entityType Validated class constructor.
2123
+ * @returns Aggregate, Projection, or Process Manager.
2124
+ */
2125
+ requireEntityFamily(entityType) {
2126
+ const family = RepositoryIdentity.resolveRepositoryEntityFamily(entityType);
2127
+ if (family !== undefined)
2128
+ return family;
2129
+ throw new RepositoryIdentityError("UNSUPPORTED_ENTITY_TYPE", `Repository entity type "${RepositoryIdentity.entityTypeName(entityType)}" must extend ` +
2130
+ "Aggregate, Projection, or ProcessManager.");
2131
+ },
2064
2132
  /**
2065
2133
  * Creates a frozen identity snapshot from validated Entity metadata.
2066
2134
  *
@@ -2408,9 +2476,8 @@ const RepositoryEntities = {
2408
2476
  * @returns The Entity instance for one repository dispatch.
2409
2477
  */
2410
2478
  instantiate(repository, entityId, stored, resetDeleted) {
2411
- const entityType = repository.entityType;
2412
2479
  const fresh = stored === undefined || resetDeleted;
2413
- return new entityType({
2480
+ return RepositoryCreation.create(repository, {
2414
2481
  id: entityId,
2415
2482
  schema: repository.stateSchema,
2416
2483
  state: fresh