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

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.
Files changed (44) hide show
  1. package/README.md +18 -0
  2. package/REFERENCE.md +93 -7
  3. package/dist/entity/entity-transaction.d.ts +46 -42
  4. package/dist/entity/entity-transaction.d.ts.map +1 -1
  5. package/dist/entity/entity-transaction.js +130 -35
  6. package/dist/entity/entity-transaction.js.map +1 -1
  7. package/dist/entity/entity.d.ts +132 -84
  8. package/dist/entity/entity.d.ts.map +1 -1
  9. package/dist/entity/entity.js +213 -217
  10. package/dist/entity/entity.js.map +1 -1
  11. package/dist/handler/event-registration-readiness.d.ts +13 -56
  12. package/dist/handler/event-registration-readiness.d.ts.map +1 -1
  13. package/dist/handler/event-registration-readiness.js +23 -43
  14. package/dist/handler/event-registration-readiness.js.map +1 -1
  15. package/dist/handler/generated-handler-registry.d.ts +8 -0
  16. package/dist/handler/generated-handler-registry.d.ts.map +1 -1
  17. package/dist/handler/generated-handler-registry.js +165 -3
  18. package/dist/handler/generated-handler-registry.js.map +1 -1
  19. package/dist/handler/handler-decorators.d.ts +70 -33
  20. package/dist/handler/handler-decorators.d.ts.map +1 -1
  21. package/dist/handler/handler-decorators.js +88 -31
  22. package/dist/handler/handler-decorators.js.map +1 -1
  23. package/dist/handler/handler-metadata.d.ts +53 -51
  24. package/dist/handler/handler-metadata.d.ts.map +1 -1
  25. package/dist/handler/handler-metadata.js +214 -42
  26. package/dist/handler/handler-metadata.js.map +1 -1
  27. package/dist/handler/registration-readiness-metadata.d.ts +4 -0
  28. package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
  29. package/dist/handler/registration-readiness-metadata.js +82 -1
  30. package/dist/handler/registration-readiness-metadata.js.map +1 -1
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/repository/repository.d.ts +137 -20
  36. package/dist/repository/repository.d.ts.map +1 -1
  37. package/dist/repository/repository.js +2589 -282
  38. package/dist/repository/repository.js.map +1 -1
  39. package/dist/runtime/standalone-handler-runtime.d.ts +16 -0
  40. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
  41. package/dist/runtime/standalone-handler-runtime.js +93 -18
  42. package/dist/runtime/standalone-handler-runtime.js.map +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +12 -12
@@ -84,8 +84,8 @@ export class RepositoryIdentityError extends Error {
84
84
  * and write changed projection state through the context-owned `Stand`. With
85
85
  * authentic process-manager metadata, built contexts can execute command
86
86
  * assignees, event reactors, and event-commanding handlers, storing changed
87
- * process-manager state through tenant-scoped Stand records with numeric
88
- * versions. The `events` option declares generated event schemas emitted by
87
+ * process-manager state through tenant-scoped Stand records with full Spine
88
+ * Versions. The `events` option declares generated event schemas emitted by
89
89
  * aggregate and process-manager producer handlers. The repository surface still
90
90
  * does not expose direct entity lookup/storage APIs, inbox/delivery management,
91
91
  * caches, lifecycle monitors, or transport startup.
@@ -271,9 +271,21 @@ Object.freeze(Repository);
271
271
  * @internal
272
272
  */
273
273
  export const repositoryAccess = Object.freeze({
274
+ /**
275
+ * Checks whether a value has registered repository identity metadata.
276
+ *
277
+ * @param repository Value to inspect.
278
+ * @returns Whether the value is a repository view.
279
+ */
274
280
  hasInstance(repository) {
275
281
  return repositorySnapshots.has(repository);
276
282
  },
283
+ /**
284
+ * Copies a repository's immutable identity metadata.
285
+ *
286
+ * @param repository Repository to inspect.
287
+ * @returns A copy-safe identity snapshot.
288
+ */
277
289
  snapshot(repository) {
278
290
  const snapshot = repositorySnapshots.get(repository);
279
291
  if (snapshot === undefined) {
@@ -281,6 +293,12 @@ export const repositoryAccess = Object.freeze({
281
293
  }
282
294
  return RepositoryIdentity.cloneRepositorySnapshot(snapshot);
283
295
  },
296
+ /**
297
+ * Reads schemas for Events this repository can emit.
298
+ *
299
+ * @param repository Repository to inspect.
300
+ * @returns Registered produced Event schemas.
301
+ */
284
302
  producedEventSchemas(repository) {
285
303
  const schemas = repositoryProducedEventSchemas.get(repository);
286
304
  if (schemas === undefined) {
@@ -288,15 +306,39 @@ export const repositoryAccess = Object.freeze({
288
306
  }
289
307
  return schemas;
290
308
  },
309
+ /**
310
+ * Looks up command dispatch for the repository.
311
+ *
312
+ * @param repository Repository to inspect.
313
+ * @returns A dispatcher when command routes are registered.
314
+ */
291
315
  commandDispatcher(repository) {
292
316
  return repositoryDispatchers.get(repository)?.command;
293
317
  },
318
+ /**
319
+ * Looks up Event dispatch for the repository.
320
+ *
321
+ * @param repository Repository to inspect.
322
+ * @returns A dispatcher when Event routes are registered.
323
+ */
294
324
  eventDispatcher(repository) {
295
325
  return repositoryDispatchers.get(repository)?.event;
296
326
  },
327
+ /**
328
+ * Looks up state-update System Event dispatch for the repository.
329
+ *
330
+ * @param repository Repository to inspect.
331
+ * @returns A dispatcher when state subscriptions are registered.
332
+ */
297
333
  systemEventDispatcher(repository) {
298
334
  return repositoryDispatchers.get(repository)?.systemEvent;
299
335
  },
336
+ /**
337
+ * Lists Entity state types subscribed by this repository.
338
+ *
339
+ * @param repository Repository to inspect.
340
+ * @returns A frozen list of state type names.
341
+ */
300
342
  stateSubscriptionTypes(repository) {
301
343
  const routing = repositoryRoutings.get(repository);
302
344
  if (routing === undefined) {
@@ -304,6 +346,13 @@ export const repositoryAccess = Object.freeze({
304
346
  }
305
347
  return Object.freeze(routing.stateSchemas.map((schema) => schema.typeName));
306
348
  },
349
+ /**
350
+ * Resolves an Entity state-change route for a Projection repository.
351
+ *
352
+ * @param repository Repository to route through.
353
+ * @param event Entity state-change System Event.
354
+ * @returns The matching route, or `undefined` for an unrelated state.
355
+ */
307
356
  routeStateUpdate(repository, event) {
308
357
  const routing = repositoryRoutings.get(repository);
309
358
  if (routing === undefined) {
@@ -311,12 +360,32 @@ export const repositoryAccess = Object.freeze({
311
360
  }
312
361
  return routing.routeStateUpdate(event);
313
362
  },
363
+ /**
364
+ * Looks up the Aggregate or Process Manager inbox replay target.
365
+ *
366
+ * @param repository Repository to inspect.
367
+ * @returns Its replay target, when registered.
368
+ */
314
369
  entityInboxTarget(repository) {
315
370
  return repositoryEntityInboxTargets.get(repository);
316
371
  },
372
+ /**
373
+ * Looks up the Projection inbox replay target.
374
+ *
375
+ * @param repository Repository to inspect.
376
+ * @returns Its replay target, when registered.
377
+ */
317
378
  projectionInboxTarget(repository) {
318
379
  return repositoryProjectionInboxTargets.get(repository);
319
380
  },
381
+ /**
382
+ * Dispatches an Event directly to a registered Projection.
383
+ *
384
+ * @param repository Projection repository to dispatch through.
385
+ * @param event Event to deliver.
386
+ * @param rebuild Whether to load deleted state for rebuilding.
387
+ * @returns Completion after the Projection receives the Event.
388
+ */
320
389
  dispatchProjectionDirect(repository, event, rebuild) {
321
390
  const dispatch = repositoryProjectionDirect.get(repository);
322
391
  if (dispatch === undefined) {
@@ -324,9 +393,20 @@ export const repositoryAccess = Object.freeze({
324
393
  }
325
394
  return dispatch(event, rebuild);
326
395
  },
396
+ /**
397
+ * Stores a built context runtime for repository dispatch.
398
+ *
399
+ * @param repository Repository receiving runtime services.
400
+ * @param runtime Context runtime to bind.
401
+ */
327
402
  bindRuntime(repository, runtime) {
328
403
  repositoryRuntimes.set(repository, Object.freeze(runtime));
329
404
  },
405
+ /**
406
+ * Removes runtime dispatch state and closes tracked storage handles.
407
+ *
408
+ * @param repository Repository to detach.
409
+ */
330
410
  clearRuntime(repository) {
331
411
  repositoryRuntimes.delete(repository);
332
412
  repositoryDispatchGuards.delete(repository);
@@ -339,15 +419,31 @@ export const repositoryAccess = Object.freeze({
339
419
  },
340
420
  });
341
421
  const inboxDedupMs = 30_000;
422
+ /**
423
+ * Loads and persists Aggregate state within one tenant storage context.
424
+ */
342
425
  class AggregateExecutionSupport {
343
426
  #repository;
344
427
  #runtime;
345
428
  #storageContext;
429
+ /**
430
+ * Binds an Aggregate repository to runtime and tenant storage services.
431
+ *
432
+ * @param repository Aggregate repository being executed.
433
+ * @param runtime Built context services.
434
+ * @param storageContext Tenant-aware storage location.
435
+ */
346
436
  constructor(repository, runtime, storageContext) {
347
437
  this.#repository = repository;
348
438
  this.#runtime = runtime;
349
439
  this.#storageContext = storageContext;
350
440
  }
441
+ /**
442
+ * Restores an Aggregate from Stand or creates its initial state and binds history.
443
+ *
444
+ * @param entityId Aggregate identifier to load.
445
+ * @returns Aggregate instance, current record, history ports, and version.
446
+ */
351
447
  async loadAggregate(entityId) {
352
448
  const storageInput = RepositoryStorage.entityStorageInput(this.#repository, this.#storageContext);
353
449
  const storage = RepositoryStorage.openRepositoryEntityStorage(this.#repository, this.#runtime.storageFactory, storageInput);
@@ -371,53 +467,55 @@ class AggregateExecutionSupport {
371
467
  storageInput,
372
468
  });
373
469
  }
470
+ /**
471
+ * Normalizes a handler result to a frozen signal list.
472
+ *
473
+ * @param produced Raw handler result.
474
+ * @returns No signals, one signal, or the copied result array.
475
+ */
374
476
  normalizeProducedSignals(produced) {
375
477
  if (produced === undefined) {
376
478
  return Object.freeze([]);
377
479
  }
378
480
  if (Array.isArray(produced)) {
379
- return Object.freeze(Array.from(produced));
481
+ return Object.freeze(Array.from(produced).filter((item) => item !== undefined));
380
482
  }
381
483
  return Object.freeze([produced]);
382
484
  }
383
485
  /**
384
- * Store the current aggregate record before its diagnostic and delivery journals.
486
+ * Stores Aggregate state and diagnostic and delivery Events in one atomic commit.
487
+ *
488
+ * @param loaded Aggregate and commit storage to update.
489
+ * @param entityId Aggregate identifier.
490
+ * @param events Produced Events to retain.
491
+ * @param diagnostics Events to retain in the diagnostic history.
492
+ * @returns `true` after a successful commit; conflicts throw.
385
493
  */
386
- async persistAggregateUpdate(loaded, entityId, version, events) {
494
+ async persistAggregateUpdate(loaded, entityId, events, diagnostics = events) {
387
495
  const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
388
496
  const state = RepositoryEntities.repositoryState(loaded.entity);
389
- const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(this.#storageContext.tenantId, create(VersionSchema, { number: RepositorySignals.eventVersionNumber(version) }), lifecycle));
497
+ const versionMessage = RepositoryEntities.repositoryVersion(loaded.entity);
498
+ const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(this.#storageContext.tenantId, versionMessage, lifecycle));
390
499
  try {
391
- const createdAt = events[events.length - 1]?.context?.timestamp ?? create(TimestampSchema);
392
- const outcome = await loaded.commits.commit({
393
- context: this.#storageContext,
394
- entity: loaded.storageInput,
395
- entityId,
396
- ...(loaded.current === undefined ? {} : { expected: loaded.current }),
397
- next: EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle),
398
- ...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
399
- ? {
400
- states: [
401
- EntityRecords.pack(this.#repository.stateSchema, entityId, state, create(VersionSchema, {
402
- number: RepositorySignals.eventVersionNumber(version),
403
- timestamp: createdAt,
404
- }), lifecycle),
405
- ],
406
- }
407
- : {}),
408
- diagnostics: events.map((event) => clone(EventSchema, event)),
409
- events,
410
- });
411
- if (outcome !== "committed") {
412
- deferred.cancel();
500
+ const outcome = await this.#commitAggregateRecord(loaded, entityId, state, versionMessage, lifecycle, events, diagnostics);
501
+ if (outcome !== "committed")
413
502
  throw new Error("Concurrent Aggregate state commit conflict.");
414
- }
415
503
  entityStateHistoryCaches.get(loaded.entity)?.clear();
416
504
  }
417
505
  catch (error) {
418
506
  deferred.cancel();
419
507
  throw error;
420
508
  }
509
+ this.#notifyAggregate(deferred, events);
510
+ return true;
511
+ }
512
+ /**
513
+ * Notifies Stand after an Aggregate commit and reports delivery failure.
514
+ *
515
+ * @param deferred Deferred Stand update created before the commit.
516
+ * @param events Produced Events available for failure reporting.
517
+ */
518
+ #notifyAggregate(deferred, events) {
421
519
  try {
422
520
  deferred.notify();
423
521
  }
@@ -426,16 +524,58 @@ class AggregateExecutionSupport {
426
524
  if (event !== undefined)
427
525
  this.#runtime.publisher.reportFailure("event", event, error);
428
526
  }
429
- return true;
430
527
  }
528
+ /**
529
+ * Writes one Aggregate state and its optional history with the same Version.
530
+ *
531
+ * @param loaded Loaded Aggregate and commit storage.
532
+ * @param entityId Aggregate identifier.
533
+ * @param state Accepted Aggregate state.
534
+ * @param version Current Spine Version.
535
+ * @param lifecycle Accepted lifecycle flags.
536
+ * @param events Produced Events to retain and publish.
537
+ * @param diagnostics Events to retain in the diagnostic history.
538
+ * @returns The storage commit outcome.
539
+ */
540
+ #commitAggregateRecord(loaded, entityId, state, version, lifecycle, events, diagnostics) {
541
+ const record = EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle);
542
+ return loaded.commits.commit({
543
+ context: this.#storageContext,
544
+ entity: loaded.storageInput,
545
+ entityId,
546
+ ...(loaded.current === undefined ? {} : { expected: loaded.current }),
547
+ next: record,
548
+ ...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
549
+ ? { states: [record] }
550
+ : {}),
551
+ diagnostics: diagnostics.map((event) => clone(EventSchema, event)),
552
+ events,
553
+ });
554
+ }
555
+ /**
556
+ * Writes a copied Event to the Aggregate diagnostic history.
557
+ *
558
+ * @param loaded Aggregate with its history port.
559
+ * @param entityId Aggregate identifier retained for the caller contract.
560
+ * @param event Event to append.
561
+ * @returns Completion after the history append.
562
+ */
431
563
  async appendDiagnosticEvent(loaded, entityId, event) {
432
564
  await loaded.events.append(clone(EventSchema, event));
433
565
  }
434
566
  /**
435
- * Complete persistence before scheduling best-effort stored-event dispatch.
567
+ * Completes persistence before scheduling best-effort stored-Event dispatch.
568
+ *
569
+ * @param loaded Aggregate and commit storage to update.
570
+ * @param entityId Aggregate identifier.
571
+ * @param events Produced Events to persist and dispatch.
572
+ * @param dispatch Publishes one committed Event.
573
+ * @param onPersisted Runs after durable persistence.
574
+ * @param diagnostics Events to retain in diagnostic history with this commit.
575
+ * @returns A deferred follow-up that dispatches the Events.
436
576
  */
437
- async persistAggregateAndDispatch(loaded, entityId, version, events, dispatch, onPersisted) {
438
- const committed = await this.persistAggregateUpdate(loaded, entityId, version, events);
577
+ async persistAggregateAndDispatch(loaded, entityId, events, dispatch, onPersisted, diagnostics = events) {
578
+ const committed = await this.persistAggregateUpdate(loaded, entityId, events, diagnostics);
439
579
  if (!committed)
440
580
  return () => Promise.resolve();
441
581
  await onPersisted();
@@ -450,31 +590,55 @@ class AggregateExecutionSupport {
450
590
  }));
451
591
  };
452
592
  }
593
+ /**
594
+ * Restores an Aggregate or creates one with initial state and version zero.
595
+ *
596
+ * @param entityId Aggregate identifier.
597
+ * @param current Stored state and lifecycle, when present.
598
+ * @returns The constructed Aggregate instance.
599
+ */
453
600
  #instantiateAggregate(entityId, current) {
454
601
  const entityType = this.#repository.entityType;
455
602
  const options = {
456
603
  id: entityId,
457
604
  schema: this.#repository.stateSchema,
458
605
  state: current?.state ?? this.#defaultState(entityId),
459
- version: current?.version ?? 0n,
606
+ version: current?.versionMessage ?? create(VersionSchema),
460
607
  };
461
608
  if (current !== undefined) {
462
609
  options.lifecycle = { archived: current.archived, deleted: current.deleted };
463
610
  }
464
611
  return new entityType(options);
465
612
  }
613
+ /**
614
+ * Creates generated initial state with its canonical identifier field set.
615
+ *
616
+ * @param entityId Identifier to place in the state.
617
+ * @returns A new state message.
618
+ */
466
619
  #defaultState(entityId) {
467
620
  return create(this.#repository.stateSchema, {
468
621
  [this.#repository.idField.localName]: entityId,
469
622
  });
470
623
  }
471
624
  }
625
+ /**
626
+ * Executes one routed Aggregate Command and stores its resulting Events.
627
+ */
472
628
  class AggregateCommandExecution {
473
629
  #repository;
474
630
  #routing;
475
631
  #runtime;
476
632
  #command;
477
633
  #support;
634
+ /**
635
+ * Captures routing, runtime, Command, and tenant-scoped persistence services.
636
+ *
637
+ * @param repository Aggregate repository receiving the Command.
638
+ * @param routing Registered Command routes and schemas.
639
+ * @param runtime Built context services.
640
+ * @param command Command to execute.
641
+ */
478
642
  constructor(repository, routing, runtime, command) {
479
643
  this.#repository = repository;
480
644
  this.#routing = routing;
@@ -482,6 +646,12 @@ class AggregateCommandExecution {
482
646
  this.#command = command;
483
647
  this.#support = new AggregateExecutionSupport(repository, runtime, RepositoryTenants.storageContextForCommand(this.#runtime.context, this.#command));
484
648
  }
649
+ /**
650
+ * Validates the Command and executes its assignee when registered.
651
+ *
652
+ * @param replayedRoute Accepted route from durable inbox replay, when present.
653
+ * @returns A deferred dispatch follow-up, or `undefined` without an assignee.
654
+ */
485
655
  async run(replayedRoute) {
486
656
  void RepositorySignals.requireCommandId(this.#command);
487
657
  const intake = this.#readIntake(replayedRoute);
@@ -489,6 +659,12 @@ class AggregateCommandExecution {
489
659
  return undefined;
490
660
  return this.#runAssignee(intake);
491
661
  }
662
+ /**
663
+ * Decodes the Command and selects its registered assignee.
664
+ *
665
+ * @param replayedRoute Accepted inbox route, when present.
666
+ * @returns Decoded message and route with an assignee, or `undefined`.
667
+ */
492
668
  #readIntake(replayedRoute) {
493
669
  const commandMessage = EntityInvocation.requireSignalMessage(this.#command.message, "command");
494
670
  const commandSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.commandSchemas, commandMessage.typeUrl, "command");
@@ -497,36 +673,57 @@ class AggregateCommandExecution {
497
673
  const assignee = this.#routing.commandReadiness?.findCommandAssignee(route.messageFullTypeName);
498
674
  return assignee === undefined ? undefined : Object.freeze({ message, route, assignee });
499
675
  }
676
+ /**
677
+ * Invokes the assignee, persists its Events, or publishes a declared rejection.
678
+ *
679
+ * @param intake Decoded Command, target route, and assignee.
680
+ * @returns A deferred Event follow-up or a rejection follow-up.
681
+ */
500
682
  async #runAssignee(intake) {
501
683
  const loaded = await this.#support.loadAggregate(intake.route.entityId);
502
684
  this.#publishCommandDispatch(intake.route.entityId);
503
- const commandContext = EntityInvocation.commandHandlerContext(this.#command);
504
685
  let produced;
505
686
  try {
506
- produced = await this.#invokeAssignee(loaded.entity, intake.assignee.handler.methodName, intake.message, intake.assignee.handler.parameterCount, commandContext);
687
+ produced = await this.#invokeAssignee(loaded.entity, intake.assignee.handler, intake.message, EntityInvocation.commandHandlerContext(this.#command));
507
688
  }
508
689
  catch (error) {
509
- if (!RejectionThrowable.is(error)) {
510
- throw error;
511
- }
512
- RepositorySignals.requireDeclaredRejection(intake.assignee.handler, error);
513
- return RepositorySignals.postRejectionEvent(this.#runtime, this.#repository, this.#command, intake.route.entityId, error);
690
+ return this.#postAssigneeRejection(intake, error);
514
691
  }
515
- const events = this.#requiredEvents(produced, intake.route.entityId, loaded.version);
692
+ const events = this.#bindProducedEvents(this.#support.normalizeProducedSignals(produced), intake.route.entityId, RepositoryEntities.priorVersion(loaded.current));
516
693
  return this.#persistAssigneeResult(loaded, intake.route.entityId, events);
517
694
  }
695
+ /**
696
+ * Publishes a declared rejection from the invoked Aggregate assignee.
697
+ *
698
+ * @param intake Assignee and target route for the rejected Command.
699
+ * @param error Handler failure to classify and publish.
700
+ * @returns A rejection follow-up when one is created.
701
+ */
702
+ #postAssigneeRejection(intake, error) {
703
+ if (!RejectionThrowable.is(error))
704
+ throw error;
705
+ RepositorySignals.requireDeclaredRejection(intake.assignee.handler, error);
706
+ return RepositorySignals.postRejectionEvent(this.#runtime, this.#repository, this.#command, intake.route.entityId, error);
707
+ }
708
+ /**
709
+ * Publishes a best-effort Command dispatch diagnostic.
710
+ *
711
+ * @param entityId Target Aggregate identifier.
712
+ */
518
713
  #publishCommandDispatch(entityId) {
519
714
  HandlerDispatchPublisher.command(this.#runtime, this.#repository, this.#command, entityId);
520
715
  }
521
- #requiredEvents(produced, entityId, version) {
522
- const events = this.#bindProducedEvents(this.#support.normalizeProducedSignals(produced), entityId, version, true);
523
- if (events.length === 0)
524
- throw new Error("Repository aggregate command handlers must return at least one event.");
525
- return events;
526
- }
716
+ /**
717
+ * Stores accepted Aggregate changes and publishes a state-change diagnostic.
718
+ *
719
+ * @param loaded Aggregate and commit storage.
720
+ * @param entityId Target Aggregate identifier.
721
+ * @param events Bound Events from the assignee.
722
+ * @returns A deferred committed-Event dispatch follow-up.
723
+ */
527
724
  #persistAssigneeResult(loaded, entityId, events) {
528
725
  const committedVersion = loaded.version + 1n;
529
- return this.#support.persistAggregateAndDispatch(loaded, entityId, committedVersion, events, (event) => this.#runtime.publisher.publishCommittedEvent(event), () => {
726
+ return this.#support.persistAggregateAndDispatch(loaded, entityId, events, (event) => this.#runtime.publisher.publishCommittedEvent(event), () => {
530
727
  if (!RepositoryEntities.repositoryChanged(loaded.entity))
531
728
  return;
532
729
  EntityStateChangePublisher.command(this.#runtime, this.#repository, this.#command, entityId, loaded.oldState, loaded.current === undefined
@@ -537,11 +734,24 @@ class AggregateCommandExecution {
537
734
  }, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositorySignals.eventVersionNumber(committedVersion));
538
735
  });
539
736
  }
540
- async #invokeAssignee(entity, methodName, message, parameterCount, context) {
737
+ /**
738
+ * Invokes an assignee in a fenced Entity transaction with output validation.
739
+ *
740
+ * @param entity Aggregate instance to mutate.
741
+ * @param handler Assignee declaration to invoke.
742
+ * @param message Decoded Command message.
743
+ * @param context Copied Command context for the handler.
744
+ * @returns The handler's raw result after a successful commit.
745
+ */
746
+ async #invokeAssignee(entity, handler, message, context) {
541
747
  transactionalEntityAccess.start(entity);
542
748
  try {
543
- const produced = await EntityInvocation.invokeEntityMethod(entity, methodName, message, parameterCount, context);
544
- const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
749
+ const produced = await EntityInvocation.invokeEntityMethod(entity, handler.methodName, message, handler.parameterCount, context);
750
+ const signals = this.#support.normalizeProducedSignals(produced);
751
+ if (signals.length === 0)
752
+ throw new Error("Repository aggregate command handlers must return at least one event.");
753
+ RepositoryHandlers.requireDeclaredOutputs(handler, signals);
754
+ const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current, true));
545
755
  if (commit.status === "rejected") {
546
756
  throw new TransitionValidationError(commit.validation.error);
547
757
  }
@@ -552,20 +762,41 @@ class AggregateCommandExecution {
552
762
  throw error;
553
763
  }
554
764
  }
555
- #bindProducedEvents(produced, entityId, lastVersion, allowEnvelopes) {
765
+ /**
766
+ * Binds each produced result to the target and pre-dispatch version.
767
+ *
768
+ * @param produced Handler results to bind.
769
+ * @param entityId Target Aggregate identifier.
770
+ * @param lastVersion Producer version before this dispatch.
771
+ * @returns Frozen bound Event list.
772
+ */
773
+ #bindProducedEvents(produced, entityId, lastVersion) {
556
774
  const dispatchVersion = lastVersion;
557
- return Object.freeze(produced.map((signal) => this.#bindProducedEvent(signal, entityId, dispatchVersion, allowEnvelopes)));
775
+ return Object.freeze(produced.map((signal) => this.#bindProducedEvent(signal, entityId, dispatchVersion)));
558
776
  }
559
- #bindProducedEvent(signal, entityId, version, allowEnvelopes) {
777
+ /**
778
+ * Packs a domain Event with producer context.
779
+ *
780
+ * @param signal One handler result.
781
+ * @param entityId Target Aggregate identifier.
782
+ * @param version Producer version before dispatch.
783
+ * @returns Event bound to this Aggregate.
784
+ */
785
+ #bindProducedEvent(signal, entityId, version) {
560
786
  const metadata = this.#runtime.signalMetadata.eventFromCommand(this.#command, {
561
- version: RepositorySignals.eventVersionNumber(version),
787
+ version: version.number,
562
788
  });
563
- const bound = allowEnvelopes && EntityInvocation.isEventEnvelope(signal)
564
- ? clone(EventSchema, signal)
565
- : this.#packDomainEvent(signal, metadata);
566
- bound.context = RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId);
789
+ const bound = this.#packDomainEvent(signal, metadata);
790
+ bound.context = RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId, version);
567
791
  return bound;
568
792
  }
793
+ /**
794
+ * Packs a declared domain Event message into an Event envelope.
795
+ *
796
+ * @param message Generated Event message from the handler.
797
+ * @param metadata ID and context derived from the source Command.
798
+ * @returns Packed Event envelope.
799
+ */
569
800
  #packDomainEvent(message, metadata) {
570
801
  const typeName = EntityInvocation.messageTypeName(message);
571
802
  const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
@@ -579,12 +810,23 @@ class AggregateCommandExecution {
579
810
  });
580
811
  }
581
812
  }
813
+ /**
814
+ * Runs Aggregate Event reactors and persists their state and Event results.
815
+ */
582
816
  class AggregateEventExecution {
583
817
  #repository;
584
818
  #routing;
585
819
  #runtime;
586
820
  #event;
587
821
  #support;
822
+ /**
823
+ * Captures Event routing and tenant-scoped Aggregate storage services.
824
+ *
825
+ * @param repository Aggregate repository receiving the Event.
826
+ * @param routing Registered Event routes and schemas.
827
+ * @param runtime Built context services.
828
+ * @param event Source Event to react to.
829
+ */
588
830
  constructor(repository, routing, runtime, event) {
589
831
  this.#repository = repository;
590
832
  this.#routing = routing;
@@ -592,6 +834,13 @@ class AggregateEventExecution {
592
834
  this.#event = event;
593
835
  this.#support = new AggregateExecutionSupport(repository, runtime, RepositoryTenants.storageContextForEvent(this.#runtime.context, this.#event));
594
836
  }
837
+ /**
838
+ * Executes matching reactors for one routed Aggregate target.
839
+ *
840
+ * @param entityId Target Aggregate identifier.
841
+ * @param acceptedRoute Route accepted before execution.
842
+ * @returns Completion after matching reactors run.
843
+ */
595
844
  async runTarget(entityId, acceptedRoute) {
596
845
  const intake = this.#readIntake(acceptedRoute);
597
846
  if (intake.reactors.length === 0) {
@@ -599,40 +848,74 @@ class AggregateEventExecution {
599
848
  }
600
849
  await this.#executeEntity(entityId, intake);
601
850
  }
851
+ /**
852
+ * Decodes the Event and selects reactors, including state-only handlers.
853
+ *
854
+ * @param acceptedRoute Route accepted before execution.
855
+ * @returns Decoded message, route, and matching reactors.
856
+ */
602
857
  #readIntake(acceptedRoute) {
603
858
  const eventMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
604
859
  const eventSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.eventSchemas, eventMessage.typeUrl, "event");
605
860
  const message = EntityInvocation.unpackRequired(eventMessage, eventSchema, "event");
606
861
  const route = acceptedRoute;
607
- const reactors = this.#routing
608
- .eventReactors(route.messageFullTypeName, message, this.#event.context?.external === true)
609
- .filter((reactor) => RepositoryHandlers.handlerEmittedSchemas(reactor.handler).length > 0);
862
+ const reactors = this.#routing.eventReactors(route.messageFullTypeName, message, this.#event.context?.external === true);
610
863
  return Object.freeze({
611
864
  message,
612
865
  route,
613
866
  reactors: Object.freeze([...reactors]),
614
867
  });
615
868
  }
869
+ /**
870
+ * Invokes reactors, persists their result, and journals the source Event.
871
+ *
872
+ * @param entityId Target Aggregate identifier.
873
+ * @param intake Decoded Event and matching reactors.
874
+ * @returns Completion after persistence and journaling.
875
+ */
616
876
  async #executeEntity(entityId, intake) {
617
877
  const loaded = await this.#support.loadAggregate(entityId);
618
878
  const produced = await this.#invokeHandlers(entityId, loaded, intake);
619
- await this.#persistProducedEvents(loaded, entityId, produced);
620
- await this.#support.appendDiagnosticEvent(loaded, entityId, DispatchGuards.guardedJournalEvent(this.#repository, this.#event, entityId));
879
+ const source = DispatchGuards.guardedJournalEvent(this.#repository, this.#event, entityId);
880
+ await this.#persistProducedEvents(loaded, entityId, produced, source);
621
881
  return undefined;
622
882
  }
623
- async #persistProducedEvents(loaded, entityId, produced) {
624
- if (produced.length === 0)
883
+ /**
884
+ * Stores changed state or produced Events and schedules reactor publication.
885
+ *
886
+ * @param loaded Aggregate and commit storage.
887
+ * @param entityId Target Aggregate identifier.
888
+ * @param produced Events returned by reactors.
889
+ * @param source Source Event retained for diagnostic history.
890
+ * @returns Completion after accepted state and Events are stored.
891
+ */
892
+ async #persistProducedEvents(loaded, entityId, produced, source) {
893
+ if (produced.length === 0 && !RepositoryEntities.repositoryChanged(loaded.entity)) {
894
+ await this.#support.appendDiagnosticEvent(loaded, entityId, source);
625
895
  return;
626
- const dispatch = await this.#support.persistAggregateAndDispatch(loaded, entityId, loaded.version + 1n, produced, (event) => this.#runtime.publisher.publishReactorEvent(event), () => {
896
+ }
897
+ const dispatch = await this.#support.persistAggregateAndDispatch(loaded, entityId, produced, (event) => this.#runtime.publisher.publishReactorEvent(event), () => {
627
898
  this.#publishStateChange(loaded, entityId);
628
- });
899
+ }, [source, ...produced]);
629
900
  void dispatch();
630
901
  }
902
+ /**
903
+ * Publishes an Entity state-change System Event when the Aggregate changed.
904
+ *
905
+ * @param loaded Aggregate before and after handling.
906
+ * @param entityId Target Aggregate identifier.
907
+ */
631
908
  #publishStateChange(loaded, entityId) {
632
909
  if (!RepositoryEntities.repositoryChanged(loaded.entity))
633
910
  return;
634
911
  EntityStateChangePublisher.event(this.#runtime, this.#repository, this.#event, entityId, loaded.oldState, this.#lifecycleSnapshot(loaded), RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositorySignals.eventVersionNumber(loaded.version + 1n));
635
912
  }
913
+ /**
914
+ * Reads lifecycle flags from the prior stored Aggregate record.
915
+ *
916
+ * @param loaded Aggregate with its optional prior record.
917
+ * @returns Prior flags, or `undefined` for a new Aggregate.
918
+ */
636
919
  #lifecycleSnapshot(loaded) {
637
920
  return loaded.current === undefined
638
921
  ? undefined
@@ -641,13 +924,21 @@ class AggregateEventExecution {
641
924
  deleted: loaded.current.lifecycleFlags?.deleted ?? false,
642
925
  };
643
926
  }
927
+ /**
928
+ * Invokes matching reactors in one Entity transaction and commits their changes.
929
+ *
930
+ * @param entityId Target Aggregate identifier.
931
+ * @param loaded Restored Aggregate and version.
932
+ * @param intake Decoded Event and matching reactors.
933
+ * @returns Frozen list of produced Events.
934
+ */
644
935
  async #invokeHandlers(entityId, loaded, intake) {
645
936
  const eventContext = EntityInvocation.eventHandlerContext(this.#event);
646
937
  const events = [];
647
938
  transactionalEntityAccess.start(loaded.entity);
648
939
  try {
649
940
  await this.#invokeReactors(entityId, loaded, intake, eventContext, events);
650
- await this.#commitEntity(loaded.entity);
941
+ await this.#commitEntity(loaded.entity, events.length > 0);
651
942
  return Object.freeze(events);
652
943
  }
653
944
  catch (error) {
@@ -655,23 +946,57 @@ class AggregateEventExecution {
655
946
  throw error;
656
947
  }
657
948
  }
949
+ /**
950
+ * Invokes each reactor, checks its declared output, and collects bound Events.
951
+ *
952
+ * @param entityId Target Aggregate identifier.
953
+ * @param loaded Restored Aggregate and producer version.
954
+ * @param intake Decoded Event and matching reactors.
955
+ * @param eventContext Copied source Event context.
956
+ * @param events Output list to append to.
957
+ * @returns Completion after all reactors run.
958
+ */
658
959
  async #invokeReactors(entityId, loaded, intake, eventContext, events) {
659
960
  if (intake.reactors.length > 0)
660
961
  HandlerDispatchPublisher.reactor(this.#runtime, this.#repository, this.#event, entityId);
661
962
  for (const reactor of intake.reactors) {
662
963
  const produced = await EntityInvocation.invokeEntityMethod(loaded.entity, reactor.handler.methodName, intake.message, reactor.handler.parameterCount, eventContext);
663
- events.push(...this.#bindProducedEvents(this.#support.normalizeProducedSignals(produced), entityId, loaded.version));
964
+ RepositoryHandlers.requireDeclaredOutputs(reactor.handler, this.#support.normalizeProducedSignals(produced));
965
+ events.push(...this.#bindProducedEvents(this.#support.normalizeProducedSignals(produced), entityId, RepositoryEntities.priorVersion(loaded.current)));
664
966
  }
665
967
  }
666
- async #commitEntity(entity) {
667
- const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
968
+ /**
969
+ * Applies a fenced Entity commit and raises transition rejection.
970
+ *
971
+ * @param entity Aggregate instance to commit.
972
+ * @param producedEvents Whether reactors produced Events.
973
+ * @returns Completion after a successful fenced commit.
974
+ */
975
+ async #commitEntity(entity, producedEvents) {
976
+ const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current, producedEvents));
668
977
  if (commit.status === "rejected")
669
978
  throw new TransitionValidationError(commit.validation.error);
670
979
  }
980
+ /**
981
+ * Binds reactor results using the pre-dispatch Aggregate version.
982
+ *
983
+ * @param produced Domain Event messages from reactors.
984
+ * @param entityId Target Aggregate identifier.
985
+ * @param lastVersion Producer version before this Event.
986
+ * @returns Frozen Event envelope list.
987
+ */
671
988
  #bindProducedEvents(produced, entityId, lastVersion) {
672
989
  const version = lastVersion;
673
990
  return Object.freeze(produced.map((signal) => this.#bindProducedEvent(signal, entityId, version)));
674
991
  }
992
+ /**
993
+ * Packs a declared reactor result with source and producer context.
994
+ *
995
+ * @param signal One domain Event message.
996
+ * @param entityId Target Aggregate identifier.
997
+ * @param version Producer version before dispatch.
998
+ * @returns Bound Event envelope.
999
+ */
675
1000
  #bindProducedEvent(signal, entityId, version) {
676
1001
  const typeName = EntityInvocation.messageTypeName(signal);
677
1002
  const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
@@ -679,21 +1004,33 @@ class AggregateEventExecution {
679
1004
  throw new Error(`Repository aggregate execution cannot pack event message "${typeName}".`);
680
1005
  }
681
1006
  const metadata = this.#runtime.signalMetadata.eventFromEvent(this.#event, {
682
- version: RepositorySignals.eventVersionNumber(version),
1007
+ version: version.number,
683
1008
  });
684
1009
  return create(EventSchema, {
685
1010
  id: metadata.id,
686
1011
  message: AnyMessages.pack(schema, signal),
687
- context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId),
1012
+ context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId, version),
688
1013
  });
689
1014
  }
690
1015
  }
1016
+ /**
1017
+ * Routes subscribed Events and state updates to a Projection.
1018
+ */
691
1019
  class ProjectionEventExecution {
692
1020
  #repository;
693
1021
  #routing;
694
1022
  #runtime;
695
1023
  #event;
696
1024
  #rebuild;
1025
+ /**
1026
+ * Captures Projection routing, runtime, source Event, and loading mode.
1027
+ *
1028
+ * @param repository Projection repository receiving the Event.
1029
+ * @param routing Registered Event and state routes.
1030
+ * @param runtime Built context services.
1031
+ * @param event Source Event to deliver.
1032
+ * @param rebuild Whether to restore deleted state for rebuilding.
1033
+ */
697
1034
  constructor(repository, routing, runtime, event, rebuild = false) {
698
1035
  this.#repository = repository;
699
1036
  this.#routing = routing;
@@ -701,10 +1038,28 @@ class ProjectionEventExecution {
701
1038
  this.#event = event;
702
1039
  this.#rebuild = rebuild;
703
1040
  }
1041
+ /**
1042
+ * Delivers a decoded Entity state update to one Projection target.
1043
+ *
1044
+ * @param repository Projection repository receiving the update.
1045
+ * @param routing Registered state-update routes.
1046
+ * @param runtime Built context services.
1047
+ * @param event Source state-change System Event.
1048
+ * @param entityId Target Projection identifier.
1049
+ * @param route Accepted state-update route.
1050
+ * @param subscribers State subscribers to invoke.
1051
+ * @returns Completion after the target receives the state.
1052
+ */
704
1053
  static async runStateTarget(repository, routing, runtime, event, entityId, route, subscribers) {
705
1054
  const execution = new ProjectionEventExecution(repository, routing, runtime, event);
706
1055
  await execution.#executeStateTarget(entityId, route.state, subscribers);
707
1056
  }
1057
+ /**
1058
+ * Delivers an accepted Event to the durable Projection inbox.
1059
+ *
1060
+ * @param acceptedRoute Route accepted before dispatch.
1061
+ * @returns Completion after inbox handoff.
1062
+ */
708
1063
  async run(acceptedRoute) {
709
1064
  const intake = this.#readIntake(acceptedRoute);
710
1065
  if (intake.subscribers.length === 0) {
@@ -714,6 +1069,13 @@ class ProjectionEventExecution {
714
1069
  await InboxHandoff.handoffProjectionEvent(this.#repository, this.#runtime, this.#event, entityId);
715
1070
  }
716
1071
  }
1072
+ /**
1073
+ * Delivers an accepted Event for one Projection target.
1074
+ *
1075
+ * @param entityId Target Projection identifier.
1076
+ * @param acceptedRoute Route accepted before replay.
1077
+ * @returns Completion after target delivery.
1078
+ */
717
1079
  async runTarget(entityId, acceptedRoute) {
718
1080
  const intake = this.#readIntake(acceptedRoute);
719
1081
  if (intake.subscribers.length === 0) {
@@ -721,6 +1083,12 @@ class ProjectionEventExecution {
721
1083
  }
722
1084
  await this.#executeTarget(entityId, intake.subscribers);
723
1085
  }
1086
+ /**
1087
+ * Dispatches directly to all routed Projection targets.
1088
+ *
1089
+ * @param acceptedRoute Route accepted before direct dispatch, when present.
1090
+ * @returns Completion after all targets receive the Event.
1091
+ */
724
1092
  async runDirect(acceptedRoute) {
725
1093
  const intake = this.#readIntake(acceptedRoute);
726
1094
  if (intake.subscribers.length === 0) {
@@ -730,6 +1098,13 @@ class ProjectionEventExecution {
730
1098
  await this.#executeTarget(entityId, intake.subscribers);
731
1099
  }
732
1100
  }
1101
+ /**
1102
+ * Invokes Event subscribers and stores a changed Projection state.
1103
+ *
1104
+ * @param entityId Target Projection identifier.
1105
+ * @param subscribers Selected Event subscribers.
1106
+ * @returns Completion after handling and any state commit.
1107
+ */
733
1108
  async #executeTarget(entityId, subscribers) {
734
1109
  const packedMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
735
1110
  const tenantOptions = RepositoryTenants.standTenantOptions(this.#runtime.context, this.#event);
@@ -741,6 +1116,14 @@ class ProjectionEventExecution {
741
1116
  ? undefined
742
1117
  : EntityRecords.unpack(this.#repository.stateSchema, loaded.current).state, mode);
743
1118
  }
1119
+ /**
1120
+ * Invokes state subscribers and stores a changed Projection state.
1121
+ *
1122
+ * @param entityId Target Projection identifier.
1123
+ * @param state Decoded source Entity state.
1124
+ * @param subscribers Selected state subscribers.
1125
+ * @returns Completion after handling and any state commit.
1126
+ */
744
1127
  async #executeStateTarget(entityId, state, subscribers) {
745
1128
  const tenantOptions = RepositoryTenants.standTenantOptions(this.#runtime.context, this.#event);
746
1129
  const loaded = await this.#loadProjection(entityId, tenantOptions, "stored");
@@ -749,6 +1132,12 @@ class ProjectionEventExecution {
749
1132
  ? undefined
750
1133
  : EntityRecords.unpack(this.#repository.stateSchema, loaded.current).state, "stored");
751
1134
  }
1135
+ /**
1136
+ * Resolves the route, decodes the Event, and filters its subscribers.
1137
+ *
1138
+ * @param acceptedRoute Route accepted before delivery, when present.
1139
+ * @returns Route and matching subscribers.
1140
+ */
752
1141
  #readIntake(acceptedRoute) {
753
1142
  const route = acceptedRoute ?? this.#repository.routeEvent(this.#event);
754
1143
  const packedMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
@@ -760,61 +1149,119 @@ class ProjectionEventExecution {
760
1149
  subscribers,
761
1150
  });
762
1151
  }
1152
+ /**
1153
+ * Persists changed Projection state, notifies Stand, and publishes the change.
1154
+ *
1155
+ * @param loaded Projection and commit storage.
1156
+ * @param tenantOptions Stand tenant selection.
1157
+ * @param oldState State before delivery, when present.
1158
+ * @param mode Stored or rebuild loading mode.
1159
+ * @returns Completion after a changed state is stored and published.
1160
+ */
763
1161
  async #storeIfChanged(loaded, tenantOptions, oldState, mode) {
764
1162
  if (!RepositoryEntities.repositoryChanged(loaded.entity)) {
765
1163
  return;
766
1164
  }
767
1165
  const entityId = loaded.entity.id;
768
1166
  const state = RepositoryEntities.repositoryState(loaded.entity);
769
- const version = BigInt(this.#event.context?.version?.number ?? 0);
1167
+ const version = RepositoryEntities.repositoryVersion(loaded.entity);
770
1168
  const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
771
- const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(tenantOptions.tenantId, this.#event.context?.version, lifecycle));
1169
+ const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(tenantOptions.tenantId, version, lifecycle));
1170
+ await this.#commitProjectionOrCancel(loaded, entityId, state, version, lifecycle, deferred);
1171
+ this.#notifyProjection(deferred);
1172
+ this.#publishProjectionChange(loaded, oldState, mode, state, lifecycle, version);
1173
+ }
1174
+ /**
1175
+ * Cancels a deferred Stand update when Projection storage rejects its commit.
1176
+ *
1177
+ * @param loaded Loaded Projection and commit storage.
1178
+ * @param entityId Projection identifier.
1179
+ * @param state Accepted Projection state.
1180
+ * @param version Current Spine Version.
1181
+ * @param lifecycle Accepted lifecycle flags.
1182
+ * @param deferred Deferred Stand update to cancel if storage rejects the commit.
1183
+ * @returns Completion after a successful storage commit.
1184
+ */
1185
+ async #commitProjectionOrCancel(loaded, entityId, state, version, lifecycle, deferred) {
772
1186
  try {
773
- const outcome = await loaded.commits.commit({
774
- context: loaded.storageInput.context,
775
- entity: loaded.storageInput,
776
- entityId,
777
- ...(loaded.current === undefined ? {} : { expected: loaded.current }),
778
- next: EntityRecords.pack(this.#repository.stateSchema, entityId, state, this.#event.context?.version ?? version, lifecycle),
779
- ...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
780
- ? {
781
- states: [
782
- EntityRecords.pack(this.#repository.stateSchema, entityId, state, this.#event.context?.version ?? version, lifecycle),
783
- ],
784
- }
785
- : {}),
786
- });
787
- if (outcome !== "committed") {
788
- deferred.cancel();
1187
+ const outcome = await this.#commitProjectionRecord(loaded, entityId, state, version, lifecycle);
1188
+ if (outcome !== "committed")
789
1189
  throw new Error("Concurrent Projection state commit conflict.");
790
- }
791
1190
  }
792
1191
  catch (error) {
793
1192
  deferred.cancel();
794
1193
  throw error;
795
1194
  }
1195
+ }
1196
+ /**
1197
+ * Notifies Stand after a Projection commit and reports delivery failure.
1198
+ *
1199
+ * @param deferred Deferred Stand update to notify.
1200
+ */
1201
+ #notifyProjection(deferred) {
796
1202
  try {
797
1203
  deferred.notify();
798
1204
  }
799
1205
  catch (error) {
800
1206
  this.#runtime.publisher.reportFailure("event", this.#event, error);
801
1207
  }
802
- EntityStateChangePublisher.event(this.#runtime, this.#repository, this.#event, entityId, oldState, mode === "rebuild"
1208
+ }
1209
+ /**
1210
+ * Persists one Projection update with the same Version in current and history records.
1211
+ *
1212
+ * @param loaded Loaded Projection and commit storage.
1213
+ * @param entityId Projection identifier.
1214
+ * @param state Accepted Projection state.
1215
+ * @param version Current Spine Version.
1216
+ * @param lifecycle Accepted lifecycle flags.
1217
+ * @returns The storage commit outcome.
1218
+ */
1219
+ #commitProjectionRecord(loaded, entityId, state, version, lifecycle) {
1220
+ const record = EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle);
1221
+ return loaded.commits.commit({
1222
+ context: loaded.storageInput.context,
1223
+ entity: loaded.storageInput,
1224
+ entityId,
1225
+ ...(loaded.current === undefined ? {} : { expected: loaded.current }),
1226
+ next: record,
1227
+ ...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
1228
+ ? { states: [record] }
1229
+ : {}),
1230
+ });
1231
+ }
1232
+ /**
1233
+ * Publishes a Projection state change with its committed Entity version.
1234
+ *
1235
+ * @param loaded Loaded Projection and prior record.
1236
+ * @param oldState State before dispatch, when available.
1237
+ * @param mode Normal or rebuilding delivery mode.
1238
+ * @param state Accepted Projection state.
1239
+ * @param lifecycle Accepted lifecycle flags.
1240
+ * @param version Committed Spine Version.
1241
+ */
1242
+ #publishProjectionChange(loaded, oldState, mode, state, lifecycle, version) {
1243
+ EntityStateChangePublisher.event(this.#runtime, this.#repository, this.#event, loaded.entity.id, oldState, mode === "rebuild"
803
1244
  ? lifecycle
804
1245
  : loaded.current === undefined
805
1246
  ? undefined
806
1247
  : {
807
1248
  archived: loaded.current.lifecycleFlags?.archived ?? false,
808
1249
  deleted: loaded.current.lifecycleFlags?.deleted ?? false,
809
- }, state, lifecycle, this.#event.context?.version?.number ?? 0);
1250
+ }, state, lifecycle, version.number);
810
1251
  }
1252
+ /**
1253
+ * Invokes Event subscribers in one Entity transaction.
1254
+ *
1255
+ * @param entity Projection instance to update.
1256
+ * @param subscribers Selected Event subscribers.
1257
+ * @param packedMessage Packed source Event message.
1258
+ * @returns Completion after the subscriber transaction commits.
1259
+ */
811
1260
  async #invokeSubscribers(entity, subscribers, packedMessage) {
812
1261
  transactionalEntityAccess.start(entity);
813
1262
  try {
814
1263
  for (const subscriber of subscribers) {
815
- const subscriberMessage = EntityInvocation.unpackRequired(packedMessage, subscriber.handler.schema, "event");
816
- const eventContext = EntityInvocation.eventHandlerContext(this.#event);
817
- await EntityInvocation.invokeEntityMethod(entity, subscriber.handler.methodName, subscriberMessage, subscriber.handler.parameterCount, eventContext);
1264
+ await this.#invokeSubscriber(entity, subscriber, packedMessage);
818
1265
  }
819
1266
  const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
820
1267
  if (commit.status === "rejected") {
@@ -826,12 +1273,37 @@ class ProjectionEventExecution {
826
1273
  throw error;
827
1274
  }
828
1275
  }
1276
+ /**
1277
+ * Invokes one Event subscriber and checks its raw return value.
1278
+ *
1279
+ * @param entity Projection instance to update.
1280
+ * @param subscriber Selected Event subscriber.
1281
+ * @param packedMessage Packed source Event message.
1282
+ * @returns Completion after the subscriber returns undefined.
1283
+ */
1284
+ async #invokeSubscriber(entity, subscriber, packedMessage) {
1285
+ const message = EntityInvocation.unpackRequired(packedMessage, subscriber.handler.schema, "event");
1286
+ const context = EntityInvocation.eventHandlerContext(this.#event);
1287
+ const result = await EntityInvocation.invokeEntityMethod(entity, subscriber.handler.methodName, message, subscriber.handler.parameterCount, context);
1288
+ if (result !== undefined)
1289
+ throw new Error(`Projection subscriber "${subscriber.handler.methodName}" must not return signals.`);
1290
+ }
1291
+ /**
1292
+ * Invokes state subscribers in one Entity transaction.
1293
+ *
1294
+ * @param entity Projection instance to update.
1295
+ * @param subscribers Selected state subscribers.
1296
+ * @param state Decoded source Entity state.
1297
+ * @returns Completion after the subscriber transaction commits.
1298
+ */
829
1299
  async #invokeStateSubscribers(entity, subscribers, state) {
830
1300
  transactionalEntityAccess.start(entity);
831
1301
  try {
832
1302
  for (const subscriber of subscribers) {
833
1303
  const context = EntityInvocation.eventHandlerContext(this.#event);
834
- await EntityInvocation.invokeEntityMethod(entity, subscriber.handler.methodName, state, subscriber.handler.parameterCount, context);
1304
+ const result = await EntityInvocation.invokeEntityMethod(entity, subscriber.handler.methodName, state, subscriber.handler.parameterCount, context);
1305
+ if (result !== undefined)
1306
+ throw new Error(`Projection subscriber "${subscriber.handler.methodName}" must not return signals.`);
835
1307
  }
836
1308
  const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
837
1309
  if (commit.status === "rejected")
@@ -842,41 +1314,34 @@ class ProjectionEventExecution {
842
1314
  throw error;
843
1315
  }
844
1316
  }
1317
+ /**
1318
+ * Restores or creates a Projection and binds its storage history.
1319
+ *
1320
+ * @param entityId Target Projection identifier.
1321
+ * @param options Stand tenant selection.
1322
+ * @param mode Stored or rebuild loading mode.
1323
+ * @returns Loaded Projection, current record, and commit storage.
1324
+ */
845
1325
  async #loadProjection(entityId, options, mode) {
846
1326
  const stored = await standAccess.readCurrent(this.#runtime.stand, this.#repository.stateSchema, entityId, options);
847
- const entityType = this.#repository.entityType;
848
- const entity = new entityType({
849
- id: entityId,
850
- schema: this.#repository.stateSchema,
851
- state: stored === undefined || (mode === "rebuild" && stored.deleted)
852
- ? this.#defaultState(entityId)
853
- : stored.state,
854
- version: mode === "rebuild" && stored?.deleted
855
- ? 0
856
- : RepositoryStand.projectionVersion(stored?.version),
857
- lifecycle: mode === "rebuild" && stored?.deleted
858
- ? { archived: false, deleted: false }
859
- : { archived: stored?.archived ?? false, deleted: stored?.deleted ?? false },
860
- });
861
- const storageInput = RepositoryStorage.entityStorageInput(this.#repository, RepositoryTenants.storageContextForTenant(this.#runtime.context, options.tenantId));
862
- const storage = RepositoryStorage.openRepositoryEntityStorage(this.#repository, this.#runtime.storageFactory, storageInput);
863
- RepositoryHistoryInternals.bindEntityHistory(entity, storage, entityId, this.#repository.stateSchema);
864
- return Object.freeze({
865
- commits: storage.commits,
866
- current: stored === undefined
867
- ? undefined
868
- : EntityRecords.pack(this.#repository.stateSchema, entityId, stored.state, stored.versionMessage, { archived: stored.archived, deleted: stored.deleted }),
869
- entity,
870
- storageInput,
871
- });
872
- }
873
- #defaultState(entityId) {
874
- return create(this.#repository.stateSchema, {
875
- [this.#repository.idField.localName]: entityId,
876
- });
1327
+ const entity = RepositoryEntities.instantiate(this.#repository, entityId, stored, mode === "rebuild" && stored?.deleted === true);
1328
+ const { commits, events, storageInput } = RepositoryEntities.bindStorage(this.#repository, this.#runtime, options.tenantId, entityId, entity);
1329
+ return RepositoryEntities.loaded(entity, stored, commits, events, storageInput, this.#repository.stateSchema, entityId);
877
1330
  }
878
1331
  }
1332
+ /**
1333
+ * Binds Process Manager query access to Stand for one handler invocation.
1334
+ */
879
1335
  const ProcessManagerQueries = Object.freeze({
1336
+ /**
1337
+ * Binds tenant-aware query reads and returns a release callback.
1338
+ *
1339
+ * @param entity Process Manager receiving query access.
1340
+ * @param runtime Context Stand and query services.
1341
+ * @param actorContext Actor metadata from the source signal.
1342
+ * @param tenantId Tenant for query reads, when present.
1343
+ * @returns Callback that removes the query binding.
1344
+ */
880
1345
  bind(entity, runtime, actorContext, tenantId) {
881
1346
  const context = actorContext === undefined
882
1347
  ? create(ActorContextSchema)
@@ -890,108 +1355,169 @@ const ProcessManagerQueries = Object.freeze({
890
1355
  }, context);
891
1356
  },
892
1357
  });
1358
+ /**
1359
+ * Loads and commits Process Manager state and optional diagnostic history.
1360
+ */
893
1361
  class ProcessManagerExecutionSupport {
894
1362
  #repository;
895
1363
  #runtime;
1364
+ /**
1365
+ * Binds a Process Manager repository to its runtime services.
1366
+ *
1367
+ * @param repository Process Manager repository being executed.
1368
+ * @param runtime Built context services.
1369
+ */
896
1370
  constructor(repository, runtime) {
897
1371
  this.#repository = repository;
898
1372
  this.#runtime = runtime;
899
1373
  }
1374
+ /**
1375
+ * Normalizes a handler result to a frozen signal list.
1376
+ *
1377
+ * @param produced Raw handler result.
1378
+ * @returns No signals, one signal, or the copied result array.
1379
+ */
900
1380
  normalizeProducedSignals(produced) {
901
1381
  if (produced === undefined) {
902
1382
  return Object.freeze([]);
903
1383
  }
904
1384
  if (Array.isArray(produced)) {
905
- return Object.freeze(Array.from(produced));
1385
+ return Object.freeze(Array.from(produced).filter((item) => item !== undefined));
906
1386
  }
907
1387
  return Object.freeze([produced]);
908
1388
  }
1389
+ /**
1390
+ * Restores or creates a Process Manager and binds its history storage.
1391
+ *
1392
+ * @param entityId Process Manager identifier to load.
1393
+ * @param options Stand tenant selection.
1394
+ * @returns Loaded instance, current record, and commit storage.
1395
+ */
909
1396
  async load(entityId, options) {
910
1397
  const stored = await standAccess.readCurrent(this.#runtime.stand, this.#repository.stateSchema, entityId, options);
911
- const entityType = this.#repository.entityType;
912
- const entity = new entityType({
913
- id: entityId,
914
- schema: this.#repository.stateSchema,
915
- state: stored === undefined ? this.#defaultState(entityId) : stored.state,
916
- version: RepositoryStand.projectionVersion(stored?.version),
917
- lifecycle: { archived: stored?.archived ?? false, deleted: stored?.deleted ?? false },
918
- });
919
- const storageInput = RepositoryStorage.entityStorageInput(this.#repository, RepositoryTenants.storageContextForTenant(this.#runtime.context, options.tenantId));
920
- const storage = RepositoryStorage.openRepositoryEntityStorage(this.#repository, this.#runtime.storageFactory, storageInput);
921
- RepositoryHistoryInternals.bindEntityHistory(entity, storage, entityId, this.#repository.stateSchema);
922
- return Object.freeze({
923
- commits: storage.commits,
924
- current: stored === undefined
925
- ? undefined
926
- : EntityRecords.pack(this.#repository.stateSchema, entityId, stored.state, stored.versionMessage, { archived: stored.archived, deleted: stored.deleted }),
927
- entity,
928
- storageInput,
929
- });
1398
+ const entity = RepositoryEntities.instantiate(this.#repository, entityId, stored, false);
1399
+ const { commits, events, storageInput } = RepositoryEntities.bindStorage(this.#repository, this.#runtime, options.tenantId, entityId, entity);
1400
+ return RepositoryEntities.loaded(entity, stored, commits, events, storageInput, this.#repository.stateSchema, entityId);
930
1401
  }
931
- async commit(loaded, options, createdAt, events) {
932
- const changed = RepositoryEntities.repositoryChanged(loaded.entity);
933
- if (!changed && events.length === 0)
1402
+ /**
1403
+ * Stores changed Process Manager state or produced Events under one Version.
1404
+ *
1405
+ * @param loaded Process Manager and commit storage.
1406
+ * @param options Stand tenant selection.
1407
+ * @param events Produced or diagnostic Events to retain when configured.
1408
+ * @returns `true` after a successful commit or unchanged no-op.
1409
+ */
1410
+ async commit(loaded, options, events) {
1411
+ const version = RepositoryEntities.repositoryVersion(loaded.entity);
1412
+ const priorNumber = loaded.current?.version?.number ?? 0;
1413
+ if (version.number === priorNumber) {
1414
+ await this.#appendUnchangedDiagnostics(loaded, events);
934
1415
  return true;
1416
+ }
935
1417
  const entityId = loaded.entity.id;
936
1418
  const state = RepositoryEntities.repositoryState(loaded.entity);
937
1419
  const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
938
- const version = changed
939
- ? BigInt(RepositoryStand.processManagerVersion(loaded.entity))
940
- : BigInt(loaded.current?.version?.number ?? 0);
941
- const deferred = changed
942
- ? await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(options.tenantId, create(VersionSchema, { number: Number(version) }), lifecycle))
943
- : undefined;
1420
+ const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(options.tenantId, version, lifecycle));
1421
+ await this.#storeProcessManagerRecord(loaded, entityId, state, version, lifecycle, events, deferred);
1422
+ this.#notifyProcessManager(deferred, events);
1423
+ return true;
1424
+ }
1425
+ /**
1426
+ * Writes configured Process Manager diagnostics without storing unchanged state.
1427
+ *
1428
+ * @param loaded Process Manager and its diagnostic Event history.
1429
+ * @param events Input and produced Events to retain when history is configured.
1430
+ * @returns Completion after all configured diagnostic Events are appended.
1431
+ */
1432
+ async #appendUnchangedDiagnostics(loaded, events) {
1433
+ if (!RepositoryStorage.historyConfiguration(this.#repository).processManagerEventHistory)
1434
+ return;
1435
+ for (const event of events)
1436
+ await loaded.events.append(clone(EventSchema, event));
1437
+ }
1438
+ /**
1439
+ * Stores accepted Process Manager state and cancels Stand notification on failure.
1440
+ *
1441
+ * @param loaded Process Manager and commit storage.
1442
+ * @param entityId Process Manager identifier.
1443
+ * @param state Accepted Process Manager state.
1444
+ * @param version Current Spine Version.
1445
+ * @param lifecycle Accepted lifecycle flags.
1446
+ * @param events Diagnostic Events to retain when configured.
1447
+ * @param deferred Stand update to notify or cancel.
1448
+ * @returns Completion after durable persistence.
1449
+ */
1450
+ async #storeProcessManagerRecord(loaded, entityId, state, version, lifecycle, events, deferred) {
944
1451
  try {
945
- const history = RepositoryStorage.historyConfiguration(this.#repository);
946
- const outcome = await loaded.commits.commit({
947
- context: loaded.storageInput.context,
948
- entity: loaded.storageInput,
949
- entityId,
950
- ...(loaded.current === undefined ? {} : { expected: loaded.current }),
951
- next: EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle),
952
- ...(changed && history.stateHistory
953
- ? {
954
- states: [
955
- EntityRecords.pack(this.#repository.stateSchema, entityId, state, create(VersionSchema, { number: Number(version), timestamp: createdAt }), lifecycle),
956
- ],
957
- }
958
- : {}),
959
- ...(history.processManagerEventHistory
960
- ? {
961
- diagnostics: events.map((event) => clone(EventSchema, event)),
962
- }
963
- : {}),
964
- });
1452
+ const outcome = await this.#commitProcessManagerRecord(loaded, entityId, state, version, lifecycle, events);
965
1453
  if (outcome !== "committed") {
966
- deferred?.cancel();
967
1454
  throw new Error("Concurrent Process Manager state commit conflict.");
968
1455
  }
969
1456
  entityStateHistoryCaches.get(loaded.entity)?.clear();
970
1457
  }
971
1458
  catch (error) {
972
- deferred?.cancel();
1459
+ deferred.cancel();
973
1460
  throw error;
974
1461
  }
1462
+ }
1463
+ /**
1464
+ * Stores one Process Manager version in current and optional history records.
1465
+ *
1466
+ * @param loaded Loaded Process Manager and commit storage.
1467
+ * @param entityId Process Manager identifier.
1468
+ * @param state Accepted Process Manager state.
1469
+ * @param version Current Spine Version.
1470
+ * @param lifecycle Accepted lifecycle flags.
1471
+ * @param events Diagnostic Events to retain when configured.
1472
+ * @returns The storage commit outcome.
1473
+ */
1474
+ #commitProcessManagerRecord(loaded, entityId, state, version, lifecycle, events) {
1475
+ const history = RepositoryStorage.historyConfiguration(this.#repository);
1476
+ const record = EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle);
1477
+ return loaded.commits.commit({
1478
+ context: loaded.storageInput.context,
1479
+ entity: loaded.storageInput,
1480
+ entityId,
1481
+ ...(loaded.current === undefined ? {} : { expected: loaded.current }),
1482
+ next: record,
1483
+ ...(history.stateHistory ? { states: [record] } : {}),
1484
+ ...(history.processManagerEventHistory
1485
+ ? { diagnostics: events.map((event) => clone(EventSchema, event)) }
1486
+ : {}),
1487
+ });
1488
+ }
1489
+ /**
1490
+ * Notifies Stand after a Process Manager commit and reports delivery failure.
1491
+ *
1492
+ * @param deferred Deferred Stand update to notify.
1493
+ * @param events Produced or diagnostic Events available for failure reporting.
1494
+ */
1495
+ #notifyProcessManager(deferred, events) {
975
1496
  try {
976
- deferred?.notify();
1497
+ deferred.notify();
977
1498
  }
978
1499
  catch (error) {
979
1500
  this.#runtime.publisher.reportFailure("event", events[events.length - 1] ?? create(EventSchema), error);
980
1501
  }
981
- return true;
982
- }
983
- #defaultState(entityId) {
984
- return create(this.#repository.stateSchema, {
985
- [this.#repository.idField.localName]: entityId,
986
- });
987
1502
  }
988
1503
  }
1504
+ /**
1505
+ * Executes a routed Process Manager Command and publishes its results.
1506
+ */
989
1507
  class ProcessManagerCommandExecution {
990
1508
  #repository;
991
1509
  #routing;
992
1510
  #runtime;
993
1511
  #command;
994
1512
  #support;
1513
+ /**
1514
+ * Captures Process Manager routing, runtime, and source Command.
1515
+ *
1516
+ * @param repository Process Manager repository receiving the Command.
1517
+ * @param routing Registered Command routes and schemas.
1518
+ * @param runtime Built context services.
1519
+ * @param command Source Command to execute.
1520
+ */
995
1521
  constructor(repository, routing, runtime, command) {
996
1522
  this.#repository = repository;
997
1523
  this.#routing = routing;
@@ -999,6 +1525,12 @@ class ProcessManagerCommandExecution {
999
1525
  this.#command = command;
1000
1526
  this.#support = new ProcessManagerExecutionSupport(repository, runtime);
1001
1527
  }
1528
+ /**
1529
+ * Invokes the registered Command handler and commits its results.
1530
+ *
1531
+ * @param replayedRoute Accepted route from durable inbox replay, when present.
1532
+ * @returns A deferred Command follow-up, rejection follow-up, or `undefined`.
1533
+ */
1002
1534
  async run(replayedRoute) {
1003
1535
  RepositorySignals.requireCommandId(this.#command);
1004
1536
  const intake = this.#readIntake(replayedRoute);
@@ -1018,9 +1550,20 @@ class ProcessManagerCommandExecution {
1018
1550
  return RepositorySignals.postRejectionEvent(this.#runtime, this.#repository, this.#command, intake.route.entityId, error);
1019
1551
  }
1020
1552
  }
1553
+ /**
1554
+ * Publishes a best-effort Command dispatch diagnostic.
1555
+ *
1556
+ * @param entityId Target Process Manager identifier.
1557
+ */
1021
1558
  #publishDispatch(entityId) {
1022
1559
  HandlerDispatchPublisher.command(this.#runtime, this.#repository, this.#command, entityId);
1023
1560
  }
1561
+ /**
1562
+ * Decodes a registered Command and finds its assignee.
1563
+ *
1564
+ * @param replayedRoute Accepted inbox route, when present.
1565
+ * @returns Decoded Command, route, and assignee, or `undefined`.
1566
+ */
1024
1567
  #readIntake(replayedRoute) {
1025
1568
  const commandMessage = EntityInvocation.requireSignalMessage(this.#command.message, "command");
1026
1569
  const commandSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.commandSchemas, commandMessage.typeUrl, "command");
@@ -1029,10 +1572,19 @@ class ProcessManagerCommandExecution {
1029
1572
  const assignee = this.#routing.commandReadiness?.findCommandAssignee(route.messageFullTypeName);
1030
1573
  return assignee === undefined ? undefined : Object.freeze({ assignee, message, route });
1031
1574
  }
1575
+ /**
1576
+ * Commits Process Manager changes and publishes produced Events and Commands.
1577
+ *
1578
+ * @param loaded Process Manager and commit storage.
1579
+ * @param tenantOptions Stand tenant selection.
1580
+ * @param intake Assignee and target route.
1581
+ * @param producedSignals Validated handler results.
1582
+ * @returns Deferred Command publication when Commands were produced.
1583
+ */
1032
1584
  async #commitAndPublish(loaded, tenantOptions, intake, producedSignals) {
1033
1585
  const commands = this.#commandOutputs(intake.assignee, producedSignals);
1034
- const events = this.#eventOutputs(intake.assignee, producedSignals, intake.route.entityId);
1035
- const committed = await this.#support.commit(loaded, tenantOptions, RepositorySignals.executionTimestamp(), events);
1586
+ const events = this.#eventOutputs(intake.assignee, producedSignals, intake.route.entityId, RepositoryEntities.priorVersion(loaded.current));
1587
+ const committed = await this.#support.commit(loaded, tenantOptions, events);
1036
1588
  if (!committed)
1037
1589
  return undefined;
1038
1590
  this.#publishChangedState(loaded, intake.route.entityId);
@@ -1043,6 +1595,13 @@ class ProcessManagerCommandExecution {
1043
1595
  await this.#postCommands(commands, this.#command);
1044
1596
  };
1045
1597
  }
1598
+ /**
1599
+ * Binds substitution results as Commands and requires nonempty output.
1600
+ *
1601
+ * @param assignee Invoked Command handler declaration.
1602
+ * @param produced Validated handler results.
1603
+ * @returns Bound Commands, or an empty list for assignment handlers.
1604
+ */
1046
1605
  #commandOutputs(assignee, produced) {
1047
1606
  if (assignee.handler.kind !== "command-substitution")
1048
1607
  return Object.freeze([]);
@@ -1052,11 +1611,26 @@ class ProcessManagerCommandExecution {
1052
1611
  }
1053
1612
  return commands;
1054
1613
  }
1055
- #eventOutputs(assignee, produced, entityId) {
1614
+ /**
1615
+ * Binds assignment results as Events.
1616
+ *
1617
+ * @param assignee Invoked Command handler declaration.
1618
+ * @param produced Validated handler results.
1619
+ * @param entityId Target Process Manager identifier.
1620
+ * @param version Process Manager Version before this dispatch.
1621
+ * @returns Bound Events, or an empty list for substitution handlers.
1622
+ */
1623
+ #eventOutputs(assignee, produced, entityId, version) {
1056
1624
  return assignee.handler.kind === "command-assignment"
1057
- ? this.#bindProducedEvents(produced, entityId)
1625
+ ? this.#bindProducedEvents(produced, entityId, version)
1058
1626
  : Object.freeze([]);
1059
1627
  }
1628
+ /**
1629
+ * Publishes a state-change System Event when the Process Manager changed.
1630
+ *
1631
+ * @param loaded Process Manager before and after handling.
1632
+ * @param entityId Target Process Manager identifier.
1633
+ */
1060
1634
  #publishChangedState(loaded, entityId) {
1061
1635
  if (RepositoryEntities.repositoryChanged(loaded.entity)) {
1062
1636
  EntityStateChangePublisher.command(this.#runtime, this.#repository, this.#command, entityId, loaded.current === undefined
@@ -1066,9 +1640,18 @@ class ProcessManagerCommandExecution {
1066
1640
  : {
1067
1641
  archived: loaded.current.lifecycleFlags?.archived ?? false,
1068
1642
  deleted: loaded.current.lifecycleFlags?.deleted ?? false,
1069
- }, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositoryStand.processManagerVersion(loaded.entity));
1643
+ }, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositoryEntities.repositoryVersion(loaded.entity).number);
1070
1644
  }
1071
1645
  }
1646
+ /**
1647
+ * Binds query access for one Command handler invocation.
1648
+ *
1649
+ * @param entity Process Manager instance.
1650
+ * @param assignee Registered Command handler.
1651
+ * @param message Decoded Command message.
1652
+ * @param tenantId Tenant for query reads, when present.
1653
+ * @returns Validated handler signals.
1654
+ */
1072
1655
  async #invoke(entity, assignee, message, tenantId) {
1073
1656
  const releaseQuery = ProcessManagerQueries.bind(entity, this.#runtime, this.#command.context?.actorContext, tenantId);
1074
1657
  try {
@@ -1078,43 +1661,73 @@ class ProcessManagerCommandExecution {
1078
1661
  releaseQuery();
1079
1662
  }
1080
1663
  }
1664
+ /**
1665
+ * Invokes the Command handler in a fenced Entity transaction.
1666
+ *
1667
+ * @param entity Process Manager instance to mutate.
1668
+ * @param assignee Registered Command handler.
1669
+ * @param message Decoded Command message.
1670
+ * @returns Validated handler signals after commit.
1671
+ */
1081
1672
  async #invokeCommandHandler(entity, assignee, message) {
1082
1673
  transactionalEntityAccess.start(entity);
1083
1674
  try {
1084
1675
  const produced = await EntityInvocation.invokeEntityMethod(entity, assignee.handler.methodName, message, assignee.handler.parameterCount, EntityInvocation.commandHandlerContext(this.#command));
1085
- const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
1676
+ const signals = this.#support.normalizeProducedSignals(produced);
1677
+ if (signals.length === 0)
1678
+ throw new Error("Repository Process Manager command handlers must return a signal.");
1679
+ RepositoryHandlers.requireDeclaredOutputs(assignee.handler, signals);
1680
+ const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current, assignee.handler.kind === "command-assignment" && signals.length > 0));
1086
1681
  if (commit.status === "rejected") {
1087
1682
  throw new TransitionValidationError(commit.validation.error);
1088
1683
  }
1089
- return this.#support.normalizeProducedSignals(produced);
1684
+ return signals;
1090
1685
  }
1091
1686
  catch (error) {
1092
1687
  transactionalEntityAccess.rollback(entity);
1093
1688
  throw error;
1094
1689
  }
1095
1690
  }
1096
- #bindProducedEvents(produced, entityId) {
1097
- let sequence = 0;
1098
- return Object.freeze(produced.map((signal) => {
1099
- sequence += 1;
1100
- return this.#bindProducedEvent(signal, entityId, sequence);
1101
- }));
1691
+ /**
1692
+ * Binds assignment results as Process Manager Events.
1693
+ *
1694
+ * @param produced Domain Event messages from the handler.
1695
+ * @param entityId Target Process Manager identifier.
1696
+ * @param version Process Manager Version before this dispatch.
1697
+ * @returns Frozen Event envelope list.
1698
+ */
1699
+ #bindProducedEvents(produced, entityId, version) {
1700
+ return Object.freeze(produced.map((signal) => this.#bindProducedEvent(signal, entityId, version)));
1102
1701
  }
1103
- #bindProducedEvent(signal, entityId, sequence) {
1702
+ /**
1703
+ * Packs one declared Event with source and producer context.
1704
+ *
1705
+ * @param signal Domain Event message to pack.
1706
+ * @param entityId Target Process Manager identifier.
1707
+ * @param version Process Manager Version before this dispatch.
1708
+ * @returns Bound Event envelope.
1709
+ */
1710
+ #bindProducedEvent(signal, entityId, version) {
1104
1711
  const typeName = EntityInvocation.messageTypeName(signal);
1105
1712
  const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
1106
1713
  if (schema === undefined) {
1107
1714
  throw new Error(`Repository process-manager execution cannot pack event message "${typeName}".`);
1108
1715
  }
1109
1716
  const metadata = this.#runtime.signalMetadata.eventFromCommand(this.#command, {
1110
- version: RepositoryStand.processManagerProducedVersion(sequence),
1717
+ version: version.number,
1111
1718
  });
1112
1719
  return create(EventSchema, {
1113
1720
  id: metadata.id,
1114
1721
  message: AnyMessages.pack(schema, signal),
1115
- context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId),
1722
+ context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId, version),
1116
1723
  });
1117
1724
  }
1725
+ /**
1726
+ * Packs declared substitution results as Commands from the source Command.
1727
+ *
1728
+ * @param produced Domain Command messages from the handler.
1729
+ * @returns Frozen Command envelope list.
1730
+ */
1118
1731
  #bindProducedCommands(produced) {
1119
1732
  return Object.freeze(produced.map((signal) => {
1120
1733
  const typeName = EntityInvocation.messageTypeName(signal);
@@ -1130,12 +1743,24 @@ class ProcessManagerCommandExecution {
1130
1743
  });
1131
1744
  }));
1132
1745
  }
1746
+ /**
1747
+ * Publishes each produced Command in handler order.
1748
+ *
1749
+ * @param commands Bound Commands to publish.
1750
+ * @param source Source Command retained for the caller contract.
1751
+ * @returns Completion after all Commands are published.
1752
+ */
1133
1753
  async #postCommands(commands, source) {
1134
1754
  void source;
1135
1755
  for (const command of commands) {
1136
1756
  await this.#runtime.publisher.publishCommand(command);
1137
1757
  }
1138
1758
  }
1759
+ /**
1760
+ * Schedules publication of produced Events.
1761
+ *
1762
+ * @param events Bound Events to publish.
1763
+ */
1139
1764
  #postEvents(events) {
1140
1765
  for (const event of events) {
1141
1766
  // spine-log-boundary: server.repository_event_follow_up
@@ -1143,12 +1768,23 @@ class ProcessManagerCommandExecution {
1143
1768
  }
1144
1769
  }
1145
1770
  }
1771
+ /**
1772
+ * Runs Process Manager Event handlers and persists their results.
1773
+ */
1146
1774
  class ProcessManagerEventExecution {
1147
1775
  #repository;
1148
1776
  #routing;
1149
1777
  #runtime;
1150
1778
  #event;
1151
1779
  #support;
1780
+ /**
1781
+ * Captures Process Manager routing, runtime, and source Event.
1782
+ *
1783
+ * @param repository Process Manager repository receiving the Event.
1784
+ * @param routing Registered Event routes and schemas.
1785
+ * @param runtime Built context services.
1786
+ * @param event Source Event to execute.
1787
+ */
1152
1788
  constructor(repository, routing, runtime, event) {
1153
1789
  this.#repository = repository;
1154
1790
  this.#routing = routing;
@@ -1156,6 +1792,12 @@ class ProcessManagerEventExecution {
1156
1792
  this.#event = event;
1157
1793
  this.#support = new ProcessManagerExecutionSupport(repository, runtime);
1158
1794
  }
1795
+ /**
1796
+ * Delivers a routed Event to the durable Process Manager inbox.
1797
+ *
1798
+ * @param acceptedRoute Route accepted before dispatch.
1799
+ * @returns Completion after inbox handoff.
1800
+ */
1159
1801
  async run(acceptedRoute) {
1160
1802
  const intake = this.#readIntake(acceptedRoute);
1161
1803
  if (intake.reactors.length === 0 && intake.commanders.length === 0) {
@@ -1168,6 +1810,13 @@ class ProcessManagerEventExecution {
1168
1810
  }
1169
1811
  await InboxHandoff.handoffPmEvents(this.#repository, this.#runtime, this.#event, intake.route.entityIds);
1170
1812
  }
1813
+ /**
1814
+ * Delivers a routed Event for one target under the duplicate-dispatch guard.
1815
+ *
1816
+ * @param entityId Target Process Manager identifier.
1817
+ * @param acceptedRoute Route accepted before replay.
1818
+ * @returns Completion after guarded target execution.
1819
+ */
1171
1820
  async runTarget(entityId, acceptedRoute) {
1172
1821
  const intake = this.#readIntake(acceptedRoute);
1173
1822
  if (intake.reactors.length === 0 && intake.commanders.length === 0) {
@@ -1178,6 +1827,12 @@ class ProcessManagerEventExecution {
1178
1827
  await this.#executeEntity(entityId, intake);
1179
1828
  });
1180
1829
  }
1830
+ /**
1831
+ * Decodes the Event and selects matching reactors and Command producers.
1832
+ *
1833
+ * @param acceptedRoute Route accepted before execution.
1834
+ * @returns Decoded message, route, reactors, and Command producers.
1835
+ */
1181
1836
  #readIntake(acceptedRoute) {
1182
1837
  const eventMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
1183
1838
  const eventSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.eventSchemas, eventMessage.typeUrl, "event");
@@ -1192,22 +1847,36 @@ class ProcessManagerEventExecution {
1192
1847
  commanders,
1193
1848
  });
1194
1849
  }
1850
+ /**
1851
+ * Invokes handlers, commits state, and publishes resulting Events and Commands.
1852
+ *
1853
+ * @param entityId Target Process Manager identifier.
1854
+ * @param intake Decoded Event and matching handlers.
1855
+ * @returns Completion after accepted results are published.
1856
+ */
1195
1857
  async #executeEntity(entityId, intake) {
1196
1858
  const tenantOptions = RepositoryTenants.standTenantOptions(this.#runtime.context, this.#event);
1197
1859
  const loaded = await this.#support.load(entityId, tenantOptions);
1198
1860
  const produced = await this.#invokeHandlers(entityId, loaded.entity, intake, tenantOptions.tenantId);
1199
- const events = this.#bindProducedEvents(produced.events, entityId);
1861
+ const events = this.#bindProducedEvents(produced.events, entityId, RepositoryEntities.priorVersion(loaded.current));
1862
+ const commands = this.#bindProducedCommands(produced.commands);
1200
1863
  const diagnostics = [
1201
1864
  DispatchGuards.guardedJournalEvent(this.#repository, this.#event, entityId),
1202
1865
  ...events,
1203
1866
  ];
1204
- const committed = await this.#support.commit(loaded, tenantOptions, this.#event.context?.timestamp ?? create(TimestampSchema), diagnostics);
1867
+ const committed = await this.#support.commit(loaded, tenantOptions, diagnostics);
1205
1868
  if (!committed)
1206
1869
  return;
1207
1870
  this.#publishChangedState(loaded, entityId);
1208
1871
  this.#postEvents(events);
1209
- await this.#postCommands(this.#bindProducedCommands(produced.commands));
1872
+ await this.#postCommands(commands);
1210
1873
  }
1874
+ /**
1875
+ * Publishes a state-change System Event when the Process Manager changed.
1876
+ *
1877
+ * @param loaded Process Manager before and after handling.
1878
+ * @param entityId Target Process Manager identifier.
1879
+ */
1211
1880
  #publishChangedState(loaded, entityId) {
1212
1881
  if (!RepositoryEntities.repositoryChanged(loaded.entity))
1213
1882
  return;
@@ -1218,8 +1887,13 @@ class ProcessManagerEventExecution {
1218
1887
  : {
1219
1888
  archived: loaded.current.lifecycleFlags?.archived ?? false,
1220
1889
  deleted: loaded.current.lifecycleFlags?.deleted ?? false,
1221
- }, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositoryStand.processManagerVersion(loaded.entity));
1890
+ }, RepositoryEntities.repositoryState(loaded.entity), RepositoryEntities.repositoryLifecycle(loaded.entity), RepositoryEntities.repositoryVersion(loaded.entity).number);
1222
1891
  }
1892
+ /**
1893
+ * Validates source Event metadata when handlers can produce follow-ups.
1894
+ *
1895
+ * @param intake Matching reactors and Command producers.
1896
+ */
1223
1897
  #validateSourceEventIdForFollowUps(intake) {
1224
1898
  const emitsFollowUpEvents = intake.reactors.some((reactor) => RepositoryHandlers.handlerEmittedSchemas(reactor.handler).length > 0);
1225
1899
  if (!emitsFollowUpEvents && intake.commanders.length === 0) {
@@ -1227,6 +1901,15 @@ class ProcessManagerEventExecution {
1227
1901
  }
1228
1902
  void this.#runtime.signalMetadata.originFromEvent(this.#event);
1229
1903
  }
1904
+ /**
1905
+ * Invokes Event handlers in one transaction with temporary query access.
1906
+ *
1907
+ * @param entityId Target Process Manager identifier.
1908
+ * @param entity Process Manager instance.
1909
+ * @param intake Decoded Event and matching handlers.
1910
+ * @param tenantId Tenant for query reads, when present.
1911
+ * @returns Validated Command and Event message lists.
1912
+ */
1230
1913
  async #invokeHandlers(entityId, entity, intake, tenantId) {
1231
1914
  const eventContext = EntityInvocation.eventHandlerContext(this.#event);
1232
1915
  const commands = [];
@@ -1235,7 +1918,7 @@ class ProcessManagerEventExecution {
1235
1918
  transactionalEntityAccess.start(entity);
1236
1919
  try {
1237
1920
  await this.#invokeEventHandlers(entityId, entity, intake, eventContext, events, commands);
1238
- await this.#commitEntity(entity);
1921
+ await this.#commitEntity(entity, events.length > 0);
1239
1922
  return Object.freeze({
1240
1923
  commands: Object.freeze(commands),
1241
1924
  events: Object.freeze(events),
@@ -1249,45 +1932,93 @@ class ProcessManagerEventExecution {
1249
1932
  releaseQuery();
1250
1933
  }
1251
1934
  }
1935
+ /**
1936
+ * Invokes reactors and Command producers, collecting their separate outputs.
1937
+ *
1938
+ * @param entityId Target Process Manager identifier.
1939
+ * @param entity Process Manager instance.
1940
+ * @param intake Decoded Event and matching handlers.
1941
+ * @param context Copied source Event context.
1942
+ * @param events Output list for Event messages.
1943
+ * @param commands Output list for Command messages.
1944
+ * @returns Completion after matching handlers run.
1945
+ */
1252
1946
  async #invokeEventHandlers(entityId, entity, intake, context, events, commands) {
1253
1947
  if (intake.reactors.length > 0)
1254
1948
  HandlerDispatchPublisher.reactor(this.#runtime, this.#repository, this.#event, entityId);
1255
1949
  await this.#invokeHandlersInto(entity, intake.reactors, intake.message, context, events);
1256
1950
  await this.#invokeHandlersInto(entity, intake.commanders, intake.message, context, commands);
1257
1951
  }
1952
+ /**
1953
+ * Invokes each selected handler and appends its declared results.
1954
+ *
1955
+ * @param entity Process Manager instance.
1956
+ * @param handlers Handler registrations to invoke.
1957
+ * @param message Decoded source Event message.
1958
+ * @param context Copied source Event context.
1959
+ * @param output Result list to append to.
1960
+ * @returns Completion after all selected handlers run.
1961
+ */
1258
1962
  async #invokeHandlersInto(entity, handlers, message, context, output) {
1259
1963
  for (const handler of handlers) {
1260
1964
  const produced = await EntityInvocation.invokeEntityMethod(entity, handler.handler.methodName, message, handler.handler.parameterCount, context);
1261
- output.push(...this.#support.normalizeProducedSignals(produced));
1965
+ const signals = this.#support.normalizeProducedSignals(produced);
1966
+ RepositoryHandlers.requireDeclaredOutputs(handler.handler, signals);
1967
+ output.push(...signals);
1262
1968
  }
1263
1969
  }
1264
- async #commitEntity(entity) {
1265
- const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current));
1970
+ /**
1971
+ * Applies a fenced Entity commit and raises transition rejection.
1972
+ *
1973
+ * @param entity Process Manager instance to commit.
1974
+ * @param producedEvents Whether handlers produced Events.
1975
+ * @returns Completion after a successful fenced commit.
1976
+ */
1977
+ async #commitEntity(entity, producedEvents) {
1978
+ const commit = await commitFenced(entity, (current) => transactionalEntityAccess.commit(current, producedEvents));
1266
1979
  if (commit.status === "rejected")
1267
1980
  throw new TransitionValidationError(commit.validation.error);
1268
1981
  }
1269
- #bindProducedEvents(produced, entityId) {
1270
- let sequence = 0;
1271
- return Object.freeze(produced.map((signal) => {
1272
- sequence += 1;
1273
- return this.#bindProducedEvent(signal, entityId, sequence);
1274
- }));
1982
+ /**
1983
+ * Binds reactor results as Process Manager Events.
1984
+ *
1985
+ * @param produced Domain Event messages from handlers.
1986
+ * @param entityId Target Process Manager identifier.
1987
+ * @param version Process Manager Version before this dispatch.
1988
+ * @returns Frozen Event envelope list.
1989
+ */
1990
+ #bindProducedEvents(produced, entityId, version) {
1991
+ return Object.freeze(produced.map((signal) => this.#bindProducedEvent(signal, entityId, version)));
1275
1992
  }
1276
- #bindProducedEvent(signal, entityId, sequence) {
1993
+ /**
1994
+ * Packs one declared Event with source and producer context.
1995
+ *
1996
+ * @param signal Domain Event message to pack.
1997
+ * @param entityId Target Process Manager identifier.
1998
+ * @param version Process Manager Version before this dispatch.
1999
+ * @returns Bound Event envelope.
2000
+ */
2001
+ #bindProducedEvent(signal, entityId, version) {
1277
2002
  const typeName = EntityInvocation.messageTypeName(signal);
1278
2003
  const schema = this.#routing.producedEventSchemas.find((candidate) => candidate.typeName === typeName);
1279
2004
  if (schema === undefined) {
1280
2005
  throw new Error(`Repository process-manager execution cannot pack event message "${typeName}".`);
1281
2006
  }
1282
2007
  const metadata = this.#runtime.signalMetadata.eventFromEvent(this.#event, {
1283
- version: RepositoryStand.processManagerProducedVersion(sequence),
2008
+ version: version.number,
1284
2009
  });
1285
2010
  return create(EventSchema, {
1286
2011
  id: metadata.id,
1287
2012
  message: AnyMessages.pack(schema, signal),
1288
- context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId),
2013
+ context: RepositorySignals.eventContextWithProducer(metadata.context, this.#repository, entityId, version),
1289
2014
  });
1290
2015
  }
2016
+ /**
2017
+ * Packs Command reactions using metadata from the source Event.
2018
+ *
2019
+ * @param produced Domain Command messages from handlers.
2020
+ * @returns Frozen Command envelope list.
2021
+ */
1291
2022
  #bindProducedCommands(produced) {
1292
2023
  return Object.freeze(produced.map((signal) => {
1293
2024
  const typeName = EntityInvocation.messageTypeName(signal);
@@ -1303,11 +2034,22 @@ class ProcessManagerEventExecution {
1303
2034
  });
1304
2035
  }));
1305
2036
  }
2037
+ /**
2038
+ * Publishes each produced Command in handler order.
2039
+ *
2040
+ * @param commands Bound Commands to publish.
2041
+ * @returns Completion after all Commands are published.
2042
+ */
1306
2043
  async #postCommands(commands) {
1307
2044
  for (const command of commands) {
1308
2045
  await this.#runtime.publisher.publishCommand(command);
1309
2046
  }
1310
2047
  }
2048
+ /**
2049
+ * Schedules publication of produced Events.
2050
+ *
2051
+ * @param events Bound Events to publish.
2052
+ */
1311
2053
  #postEvents(events) {
1312
2054
  for (const event of events) {
1313
2055
  // spine-log-boundary: server.process_manager_event_follow_up
@@ -1319,6 +2061,15 @@ class ProcessManagerEventExecution {
1319
2061
  * Internal repository identity operations.
1320
2062
  */
1321
2063
  const RepositoryIdentity = {
2064
+ /**
2065
+ * Creates a frozen identity snapshot from validated Entity metadata.
2066
+ *
2067
+ * @typeParam EntityType Concrete Entity constructor represented by the snapshot.
2068
+ * @param entityType Entity constructor.
2069
+ * @param entityFamily Aggregate, Projection, or Process Manager family.
2070
+ * @param metadata Descriptor-derived state metadata.
2071
+ * @returns Copy-safe repository identity snapshot.
2072
+ */
1322
2073
  createRepositorySnapshot(entityType, entityFamily, metadata) {
1323
2074
  const metadataCopy = RepositoryIdentity.cloneEntityMetadata(metadata);
1324
2075
  return Object.freeze({
@@ -1330,6 +2081,13 @@ const RepositoryIdentity = {
1330
2081
  idField: RepositoryIdentity.cloneFieldMetadata(metadataCopy.idField),
1331
2082
  });
1332
2083
  },
2084
+ /**
2085
+ * Copies a repository identity snapshot and its mutable metadata fields.
2086
+ *
2087
+ * @typeParam EntityType Concrete Entity constructor in the snapshot.
2088
+ * @param snapshot Identity snapshot to copy.
2089
+ * @returns Frozen copy of the snapshot.
2090
+ */
1333
2091
  cloneRepositorySnapshot(snapshot) {
1334
2092
  const metadata = RepositoryIdentity.cloneEntityMetadata(snapshot.metadata);
1335
2093
  return Object.freeze({
@@ -1341,9 +2099,21 @@ const RepositoryIdentity = {
1341
2099
  idField: RepositoryIdentity.cloneFieldMetadata(metadata.idField),
1342
2100
  });
1343
2101
  },
2102
+ /**
2103
+ * Checks that repository options are a non-null object.
2104
+ *
2105
+ * @param options Value supplied as repository options.
2106
+ * @returns Whether the value can expose option properties.
2107
+ */
1344
2108
  isRepositoryOptionsObject(options) {
1345
2109
  return typeof options === "object" && options !== null;
1346
2110
  },
2111
+ /**
2112
+ * Reads the Entity constructor option and reports inaccessible properties.
2113
+ *
2114
+ * @param options Repository options object.
2115
+ * @returns Supplied Entity constructor value.
2116
+ */
1347
2117
  readEntityTypeOption(options) {
1348
2118
  try {
1349
2119
  return options.entityType;
@@ -1353,6 +2123,14 @@ const RepositoryIdentity = {
1353
2123
  "extending Aggregate, Projection, or ProcessManager.");
1354
2124
  }
1355
2125
  },
2126
+ /**
2127
+ * Reads the state schema option and reports inaccessible properties.
2128
+ *
2129
+ * @param options Repository options object.
2130
+ * @param entityTypeDisplayName Constructor name for diagnostics.
2131
+ * @param entityFamily Resolved Entity family for diagnostics.
2132
+ * @returns Supplied state schema value.
2133
+ */
1356
2134
  readRepositorySchemaOption(options, entityTypeDisplayName, entityFamily) {
1357
2135
  try {
1358
2136
  return options.schema;
@@ -1362,6 +2140,12 @@ const RepositoryIdentity = {
1362
2140
  "the supplied state schema could not be read.");
1363
2141
  }
1364
2142
  },
2143
+ /**
2144
+ * Checks whether a value is a native class constructor.
2145
+ *
2146
+ * @param entityType Candidate Entity constructor.
2147
+ * @returns Whether the value has class syntax.
2148
+ */
1365
2149
  isClassConstructor(entityType) {
1366
2150
  if (typeof entityType !== "function") {
1367
2151
  return false;
@@ -1375,8 +2159,10 @@ const RepositoryIdentity = {
1375
2159
  }
1376
2160
  },
1377
2161
  /**
1378
- * Resolves the runtime family of a repository-owned entity constructor.
2162
+ * Resolves the runtime family of a repository Entity constructor.
1379
2163
  *
2164
+ * @param entityType Candidate Entity constructor.
2165
+ * @returns Its supported family, or `undefined`.
1380
2166
  * @internal
1381
2167
  */
1382
2168
  resolveRepositoryEntityFamily(entityType) {
@@ -1395,6 +2181,14 @@ const RepositoryIdentity = {
1395
2181
  }
1396
2182
  return undefined;
1397
2183
  },
2184
+ /**
2185
+ * Checks constructor and prototype inheritance against an Entity family.
2186
+ *
2187
+ * @param entityType Candidate constructor.
2188
+ * @param familyConstructor Aggregate, Projection, or Process Manager constructor.
2189
+ * @param familyPrototype Prototype for the same family.
2190
+ * @returns Whether both inheritance paths match.
2191
+ */
1398
2192
  hasEntityFamilyInheritance(entityType, familyConstructor, familyPrototype) {
1399
2193
  try {
1400
2194
  return (Object.prototype.isPrototypeOf.call(familyConstructor, entityType) &&
@@ -1404,6 +2198,12 @@ const RepositoryIdentity = {
1404
2198
  return false;
1405
2199
  }
1406
2200
  },
2201
+ /**
2202
+ * Reads a safe diagnostic name from an Entity constructor.
2203
+ *
2204
+ * @param entityType Constructor value to describe.
2205
+ * @returns Its name, or an anonymous placeholder.
2206
+ */
1407
2207
  entityTypeName(entityType) {
1408
2208
  if ((typeof entityType !== "object" && typeof entityType !== "function") ||
1409
2209
  entityType === null) {
@@ -1412,6 +2212,13 @@ const RepositoryIdentity = {
1412
2212
  const name = RepositoryIdentity.safeStringProperty(entityType, "name");
1413
2213
  return typeof name === "string" && name.length > 0 ? name : "(anonymous)";
1414
2214
  },
2215
+ /**
2216
+ * Reads one string property without propagating accessor errors.
2217
+ *
2218
+ * @param value Object to inspect.
2219
+ * @param propertyName Property to read.
2220
+ * @returns String value when readable, otherwise `undefined`.
2221
+ */
1415
2222
  safeStringProperty(value, propertyName) {
1416
2223
  try {
1417
2224
  const property = value[propertyName];
@@ -1421,6 +2228,15 @@ const RepositoryIdentity = {
1421
2228
  return undefined;
1422
2229
  }
1423
2230
  },
2231
+ /**
2232
+ * Describes a generated state schema and wraps invalid metadata errors.
2233
+ *
2234
+ * @typeParam Schema Concrete generated state schema.
2235
+ * @param entityTypeDisplayName Constructor name for diagnostics.
2236
+ * @param entityFamily Resolved Entity family for diagnostics.
2237
+ * @param schema Generated state schema to inspect.
2238
+ * @returns Descriptor-derived Entity metadata.
2239
+ */
1424
2240
  describeRepositoryEntityMetadata(entityTypeDisplayName, entityFamily, schema) {
1425
2241
  try {
1426
2242
  return describeEntityMetadata(schema);
@@ -1430,6 +2246,13 @@ const RepositoryIdentity = {
1430
2246
  "the supplied state schema does not expose supported entity metadata.");
1431
2247
  }
1432
2248
  },
2249
+ /**
2250
+ * Copies descriptor-derived Entity metadata and nested field descriptions.
2251
+ *
2252
+ * @typeParam Schema Generated state schema represented by the metadata.
2253
+ * @param metadata Entity metadata to copy.
2254
+ * @returns Frozen metadata copy.
2255
+ */
1433
2256
  cloneEntityMetadata(metadata) {
1434
2257
  const idField = RepositoryIdentity.cloneFieldMetadata(metadata.idField);
1435
2258
  const firstFieldRoutingHint = Object.freeze({
@@ -1452,6 +2275,12 @@ const RepositoryIdentity = {
1452
2275
  setOnceFields: Object.freeze(metadata.setOnceFields.map((field) => RepositoryIdentity.cloneFieldMetadata(field))),
1453
2276
  });
1454
2277
  },
2278
+ /**
2279
+ * Copies one descriptor field description for an identity snapshot.
2280
+ *
2281
+ * @param field Descriptor field to copy.
2282
+ * @returns Frozen field description.
2283
+ */
1455
2284
  cloneFieldMetadata(field) {
1456
2285
  return Object.freeze({
1457
2286
  descriptor: field.descriptor,
@@ -1467,11 +2296,12 @@ Object.freeze(RepositoryIdentity);
1467
2296
  * Internal entity invocation operations.
1468
2297
  */
1469
2298
  const EntityInvocation = {
1470
- isEventEnvelope(signal) {
1471
- return (typeof signal === "object" &&
1472
- signal !== null &&
1473
- signal.$typeName === EventSchema.typeName);
1474
- },
2299
+ /**
2300
+ * Reads a generated message's type name or rejects malformed output.
2301
+ *
2302
+ * @param message Handler result to inspect.
2303
+ * @returns Its generated message type name.
2304
+ */
1475
2305
  messageTypeName(message) {
1476
2306
  const typeName = message.$typeName;
1477
2307
  if (typeof typeName !== "string" || typeName.length === 0) {
@@ -1479,6 +2309,16 @@ const EntityInvocation = {
1479
2309
  }
1480
2310
  return typeName;
1481
2311
  },
2312
+ /**
2313
+ * Invokes a registered Entity handler with its declared argument count.
2314
+ *
2315
+ * @param entity Entity instance containing the method.
2316
+ * @param methodName Registered handler method name.
2317
+ * @param message Decoded Command or Event message.
2318
+ * @param parameterCount One message parameter or message plus context.
2319
+ * @param context Handler context when declared.
2320
+ * @returns Raw handler result.
2321
+ */
1482
2322
  invokeEntityMethod(entity, methodName, message, parameterCount = 1, context) {
1483
2323
  const method = entity[methodName];
1484
2324
  if (typeof method !== "function") {
@@ -1486,16 +2326,36 @@ const EntityInvocation = {
1486
2326
  }
1487
2327
  return Reflect.apply(method, entity, parameterCount === 2 ? [message, context] : [message]);
1488
2328
  },
2329
+ /**
2330
+ * Copies a Command context or creates an empty context when absent.
2331
+ *
2332
+ * @param command Source Command envelope.
2333
+ * @returns Context isolated from the source envelope.
2334
+ */
1489
2335
  commandHandlerContext(command) {
1490
2336
  return command.context === undefined
1491
2337
  ? create(CommandContextSchema)
1492
2338
  : clone(CommandContextSchema, command.context);
1493
2339
  },
2340
+ /**
2341
+ * Copies an Event context or creates an empty context when absent.
2342
+ *
2343
+ * @param event Source Event envelope.
2344
+ * @returns Context isolated from the source envelope.
2345
+ */
1494
2346
  eventHandlerContext(event) {
1495
2347
  return event.context === undefined
1496
2348
  ? create(EventContextSchema)
1497
2349
  : clone(EventContextSchema, event.context);
1498
2350
  },
2351
+ /**
2352
+ * Unpacks a required signal message with its registered schema.
2353
+ *
2354
+ * @param message Packed signal payload.
2355
+ * @param schema Registered generated message schema.
2356
+ * @param signalKind Signal name for failure diagnostics.
2357
+ * @returns Decoded generated message.
2358
+ */
1499
2359
  unpackRequired(message, schema, signalKind) {
1500
2360
  const unpacked = AnyMessages.unpack(message, schema);
1501
2361
  if (unpacked === undefined) {
@@ -1503,6 +2363,13 @@ const EntityInvocation = {
1503
2363
  }
1504
2364
  return unpacked;
1505
2365
  },
2366
+ /**
2367
+ * Validates that a signal carries a typed message payload.
2368
+ *
2369
+ * @param message Optional packed signal payload.
2370
+ * @param signalKind Signal name for failure diagnostics.
2371
+ * @returns Payload with a nonempty type URL.
2372
+ */
1506
2373
  requireSignalMessage(message, signalKind) {
1507
2374
  if (message === undefined || message.typeUrl === "") {
1508
2375
  throw new Error(`Repository ${signalKind} execution requires message.typeUrl.`);
@@ -1515,12 +2382,116 @@ Object.freeze(EntityInvocation);
1515
2382
  * Internal repository entities operations.
1516
2383
  */
1517
2384
  const RepositoryEntities = {
2385
+ /**
2386
+ * Opens Entity storage and binds diagnostic history to the restored instance.
2387
+ *
2388
+ * @param repository Entity repository registration.
2389
+ * @param runtime Storage factory and tenant mode.
2390
+ * @param tenantId Tenant identifier when multitenant.
2391
+ * @param entityId Entity identifier.
2392
+ * @param entity Restored Entity instance.
2393
+ * @returns Commit storage and its Entity storage specification.
2394
+ */
2395
+ bindStorage(repository, runtime, tenantId, entityId, entity) {
2396
+ const storageInput = RepositoryStorage.entityStorageInput(repository, RepositoryTenants.storageContextForTenant(runtime.context, tenantId));
2397
+ const storage = RepositoryStorage.openRepositoryEntityStorage(repository, runtime.storageFactory, storageInput);
2398
+ RepositoryHistoryInternals.bindEntityHistory(entity, storage, entityId, repository.stateSchema);
2399
+ return { commits: storage.commits, events: storage.events, storageInput };
2400
+ },
2401
+ /**
2402
+ * Restores an Entity from Stand or creates a fresh instance at Version zero.
2403
+ *
2404
+ * @param repository Entity schema and constructor registration.
2405
+ * @param entityId Entity identifier.
2406
+ * @param stored Prior Stand snapshot, when present.
2407
+ * @param resetDeleted Whether rebuild replaces a deleted Projection.
2408
+ * @returns The Entity instance for one repository dispatch.
2409
+ */
2410
+ instantiate(repository, entityId, stored, resetDeleted) {
2411
+ const entityType = repository.entityType;
2412
+ const fresh = stored === undefined || resetDeleted;
2413
+ return new entityType({
2414
+ id: entityId,
2415
+ schema: repository.stateSchema,
2416
+ state: fresh
2417
+ ? create(repository.stateSchema, { [repository.idField.localName]: entityId })
2418
+ : stored.state,
2419
+ version: fresh ? create(VersionSchema) : stored.versionMessage,
2420
+ lifecycle: resetDeleted
2421
+ ? { archived: false, deleted: false }
2422
+ : { archived: stored?.archived ?? false, deleted: stored?.deleted ?? false },
2423
+ });
2424
+ },
2425
+ /**
2426
+ * Builds the loaded repository Entity view from a Stand snapshot.
2427
+ *
2428
+ * @param entity Restored Entity instance.
2429
+ * @param stored Prior Stand snapshot, when present.
2430
+ * @param commits Durable commit storage.
2431
+ * @param events Durable diagnostic Event history.
2432
+ * @param storageInput Entity storage specification.
2433
+ * @param schema Entity state schema.
2434
+ * @param entityId Entity identifier.
2435
+ * @returns The loaded Entity and prior record.
2436
+ */
2437
+ loaded(entity, stored, commits, events, storageInput, schema, entityId) {
2438
+ return Object.freeze({
2439
+ commits,
2440
+ events,
2441
+ current: stored === undefined
2442
+ ? undefined
2443
+ : EntityRecords.pack(schema, entityId, stored.state, stored.versionMessage, {
2444
+ archived: stored.archived,
2445
+ deleted: stored.deleted,
2446
+ }),
2447
+ entity,
2448
+ storageInput,
2449
+ });
2450
+ },
2451
+ /**
2452
+ * Returns a defensive copy of the Version before this repository dispatch.
2453
+ *
2454
+ * @param current Prior stored Entity record, when one exists.
2455
+ * @returns Prior Version, or the initial zero Version for a fresh Entity.
2456
+ */
2457
+ priorVersion(current) {
2458
+ return current?.version === undefined
2459
+ ? create(VersionSchema)
2460
+ : clone(VersionSchema, current.version);
2461
+ },
2462
+ /**
2463
+ * Reads the current state snapshot from a repository Entity.
2464
+ *
2465
+ * @param entity Entity instance to inspect.
2466
+ * @returns Its current state message.
2467
+ */
1518
2468
  repositoryState(entity) {
1519
2469
  return entity.state;
1520
2470
  },
2471
+ /**
2472
+ * Reads the current Spine Version from a repository Entity.
2473
+ *
2474
+ * @param entity Entity instance to inspect.
2475
+ * @returns Its current Version snapshot.
2476
+ */
2477
+ repositoryVersion(entity) {
2478
+ return entity.version;
2479
+ },
2480
+ /**
2481
+ * Reads archived and deleted flags from a repository Entity.
2482
+ *
2483
+ * @param entity Entity instance to inspect.
2484
+ * @returns Its current lifecycle flags.
2485
+ */
1521
2486
  repositoryLifecycle(entity) {
1522
2487
  return entity.lifecycle;
1523
2488
  },
2489
+ /**
2490
+ * Checks whether the Entity transaction changed state or lifecycle.
2491
+ *
2492
+ * @param entity Entity instance to inspect.
2493
+ * @returns Whether the Entity reports a change.
2494
+ */
1524
2495
  repositoryChanged(entity) {
1525
2496
  return entity.changed === true;
1526
2497
  },
@@ -1530,9 +2501,21 @@ Object.freeze(RepositoryEntities);
1530
2501
  * Internal repository signals operations.
1531
2502
  */
1532
2503
  const RepositorySignals = {
2504
+ /**
2505
+ * Validates that a rejection type was declared by the invoked handler.
2506
+ *
2507
+ * @param handler Handler declaration that raised the rejection.
2508
+ * @param rejection Rejection to validate.
2509
+ */
1533
2510
  requireDeclaredRejection(handler, rejection) {
1534
2511
  DeclaredRejections.require(handler.methodName, HandlerMetadataValues.thrownSchemas(handler), rejection);
1535
2512
  },
2513
+ /**
2514
+ * Reads the producer version from an Event context.
2515
+ *
2516
+ * @param event Event carrying producer metadata.
2517
+ * @returns Version number as a bigint.
2518
+ */
1536
2519
  readEventVersion(event) {
1537
2520
  const number = event.context?.version?.number;
1538
2521
  if (number === undefined) {
@@ -1540,16 +2523,45 @@ const RepositorySignals = {
1540
2523
  }
1541
2524
  return BigInt(number);
1542
2525
  },
2526
+ /**
2527
+ * Converts an Aggregate version into the Event context int32 range.
2528
+ *
2529
+ * @param version Aggregate version number.
2530
+ * @returns Version number suitable for a Protobuf Event context.
2531
+ */
1543
2532
  eventVersionNumber(version) {
1544
2533
  if (version > 2147483647n || version < -2147483648n) {
1545
2534
  throw new Error("Repository aggregate execution requires versions in the protobuf int32 range.");
1546
2535
  }
1547
2536
  return Number(version);
1548
2537
  },
1549
- eventContextWithProducer(context, repository, entityId) {
2538
+ /**
2539
+ * Copies an Event context with the producing Entity identifier.
2540
+ *
2541
+ * @param context Event context to extend.
2542
+ * @param repository Producing Entity repository.
2543
+ * @param entityId Producing Entity identifier.
2544
+ * @param version Full pre-dispatch producer Version, when supplied.
2545
+ * @returns New Event context with producer metadata.
2546
+ */
2547
+ eventContextWithProducer(context, repository, entityId, version) {
1550
2548
  const producerId = EntityIds.pack(repository.stateSchema, entityId);
1551
- return create(EventContextSchema, { ...context, producerId });
2549
+ return create(EventContextSchema, {
2550
+ ...context,
2551
+ producerId,
2552
+ ...(version === undefined ? {} : { version: clone(VersionSchema, version) }),
2553
+ });
1552
2554
  },
2555
+ /**
2556
+ * Builds a rejection Event and returns a best-effort publication follow-up.
2557
+ *
2558
+ * @param runtime Context signal and publication services.
2559
+ * @param repository Repository of the rejected Command.
2560
+ * @param command Command rejected by its handler.
2561
+ * @param entityId Target Entity identifier.
2562
+ * @param rejection Declared handler rejection.
2563
+ * @returns Deferred rejection Event publication.
2564
+ */
1553
2565
  postRejectionEvent(runtime, repository, command, entityId, rejection) {
1554
2566
  const metadata = runtime.signalMetadata.eventFromCommand(command, {});
1555
2567
  const event = create(EventSchema, {
@@ -1573,18 +2585,35 @@ const RepositorySignals = {
1573
2585
  }
1574
2586
  };
1575
2587
  },
2588
+ /**
2589
+ * Validates a nonempty Command ID for Event origin metadata.
2590
+ *
2591
+ * @param command Source Command envelope.
2592
+ * @returns Its nonempty ID.
2593
+ */
1576
2594
  requireCommandId(command) {
1577
2595
  if (command.id === undefined || command.id.uuid.trim().length === 0) {
1578
2596
  throw new Error("Repository aggregate execution requires command.id to bind event origins.");
1579
2597
  }
1580
2598
  return command.id;
1581
2599
  },
2600
+ /**
2601
+ * Validates a nonempty Event ID for inbox deduplication.
2602
+ *
2603
+ * @param event Source Event envelope.
2604
+ * @returns Its nonempty ID.
2605
+ */
1582
2606
  requireEventId(event) {
1583
2607
  if (event.id === undefined || event.id.value.trim().length === 0) {
1584
2608
  throw new Error("Repository projection inbox handoff requires event.id.");
1585
2609
  }
1586
2610
  return event.id;
1587
2611
  },
2612
+ /**
2613
+ * Creates a Protobuf timestamp for this execution instant.
2614
+ *
2615
+ * @returns Timestamp derived from the current clock.
2616
+ */
1588
2617
  executionTimestamp() {
1589
2618
  const milliseconds = Date.now();
1590
2619
  return create(TimestampSchema, {
@@ -1597,6 +2626,19 @@ const RepositorySignals = {
1597
2626
  * Builds and best-effort dispatches committed entity state notifications.
1598
2627
  */
1599
2628
  class EntityStateChangePublishing {
2629
+ /**
2630
+ * Publishes System Events for an Entity change caused by a Command.
2631
+ *
2632
+ * @param runtime Context signal and publication services.
2633
+ * @param repository Repository of the changed Entity.
2634
+ * @param command Source Command.
2635
+ * @param entityId Changed Entity identifier.
2636
+ * @param oldState State before handling, when present.
2637
+ * @param oldLifecycle Lifecycle before handling, when present.
2638
+ * @param newState Accepted Entity state.
2639
+ * @param lifecycle Accepted lifecycle flags.
2640
+ * @param version Committed Entity version number.
2641
+ */
1600
2642
  command(runtime, repository, command, entityId, oldState, oldLifecycle, newState, lifecycle, version) {
1601
2643
  this.#publish(runtime, () => runtime.signalMetadata.eventFromCommand(command, {
1602
2644
  version,
@@ -1605,6 +2647,19 @@ class EntityStateChangePublishing {
1605
2647
  typeUrl: command.message?.typeUrl ?? "",
1606
2648
  }, { repository, entityId, oldState, oldLifecycle, newState, lifecycle, version });
1607
2649
  }
2650
+ /**
2651
+ * Publishes System Events for an Entity change caused by an Event.
2652
+ *
2653
+ * @param runtime Context signal and publication services.
2654
+ * @param repository Repository of the changed Entity.
2655
+ * @param source Source Event.
2656
+ * @param entityId Changed Entity identifier.
2657
+ * @param oldState State before handling, when present.
2658
+ * @param oldLifecycle Lifecycle before handling, when present.
2659
+ * @param newState Accepted Entity state.
2660
+ * @param lifecycle Accepted lifecycle flags.
2661
+ * @param version Committed Entity version number.
2662
+ */
1608
2663
  event(runtime, repository, source, entityId, oldState, oldLifecycle, newState, lifecycle, version) {
1609
2664
  this.#publish(runtime, () => runtime.signalMetadata.eventFromEvent(source, {
1610
2665
  version,
@@ -1613,6 +2668,14 @@ class EntityStateChangePublishing {
1613
2668
  typeUrl: source.message?.typeUrl ?? "",
1614
2669
  }, { repository, entityId, oldState, oldLifecycle, newState, lifecycle, version });
1615
2670
  }
2671
+ /**
2672
+ * Packs and publishes each System Event describing the accepted change.
2673
+ *
2674
+ * @param runtime Context signal and publication services.
2675
+ * @param metadataFor Creates Event metadata from the source signal.
2676
+ * @param origin Source signal identifier and type URL.
2677
+ * @param change Accepted Entity transition.
2678
+ */
1616
2679
  #publish(runtime, metadataFor, origin, change) {
1617
2680
  const drafts = this.#drafts(origin, change);
1618
2681
  drafts.forEach((draft) => {
@@ -1626,12 +2689,26 @@ class EntityStateChangePublishing {
1626
2689
  this.#post(runtime, event);
1627
2690
  });
1628
2691
  }
2692
+ /**
2693
+ * Creates state and lifecycle System Events for a committed change.
2694
+ *
2695
+ * @param origin Source signal identifier and type URL.
2696
+ * @param change Accepted Entity transition.
2697
+ * @returns Event drafts for the changed state or lifecycle.
2698
+ */
1629
2699
  #drafts(origin, change) {
1630
2700
  const fields = this.#fields(origin, change);
1631
2701
  const archive = this.#archiveDraft(fields, change);
1632
2702
  const deletion = this.#deleteDraft(fields, change);
1633
2703
  return [...this.#stateDrafts(fields, change), archive, deletion].filter((draft) => draft !== undefined);
1634
2704
  }
2705
+ /**
2706
+ * Packs fields shared by Entity state and lifecycle diagnostics.
2707
+ *
2708
+ * @param origin Source signal identifier and type URL.
2709
+ * @param change Accepted Entity transition.
2710
+ * @returns Shared Entity, signal, state, and version fields.
2711
+ */
1635
2712
  #fields(origin, change) {
1636
2713
  const entity = create(MessageIdSchema, {
1637
2714
  id: this.#packEntityId(change.repository, change.entityId),
@@ -1644,6 +2721,13 @@ class EntityStateChangePublishing {
1644
2721
  version: create(VersionSchema, { number: change.version }),
1645
2722
  };
1646
2723
  }
2724
+ /**
2725
+ * Creates state Event drafts from prior and next state.
2726
+ *
2727
+ * @param fields Shared System Event fields.
2728
+ * @param change Accepted Entity transition.
2729
+ * @returns Zero, one, or two state Event drafts.
2730
+ */
1647
2731
  #stateDrafts(fields, change) {
1648
2732
  const drafts = [];
1649
2733
  if (change.oldState === undefined) {
@@ -1661,6 +2745,13 @@ class EntityStateChangePublishing {
1661
2745
  }
1662
2746
  return drafts;
1663
2747
  }
2748
+ /**
2749
+ * Builds an EntityStateChanged payload draft.
2750
+ *
2751
+ * @param fields Shared System Event fields.
2752
+ * @param change Accepted Entity transition.
2753
+ * @returns Draft that receives the envelope timestamp later.
2754
+ */
1664
2755
  #stateChangedDraft(fields, change) {
1665
2756
  return {
1666
2757
  schema: EntityLog.EntityStateChangedSchema,
@@ -1678,6 +2769,13 @@ class EntityStateChangePublishing {
1678
2769
  }),
1679
2770
  };
1680
2771
  }
2772
+ /**
2773
+ * Creates an archive or unarchive Event when that flag changed.
2774
+ *
2775
+ * @param fields Shared System Event fields.
2776
+ * @param change Accepted Entity transition.
2777
+ * @returns Lifecycle Event draft, or `undefined`.
2778
+ */
1681
2779
  #archiveDraft(fields, change) {
1682
2780
  const previous = change.oldLifecycle ?? { archived: false, deleted: false };
1683
2781
  if (previous.archived === change.lifecycle.archived)
@@ -1704,6 +2802,13 @@ class EntityStateChangePublishing {
1704
2802
  }),
1705
2803
  };
1706
2804
  }
2805
+ /**
2806
+ * Creates a deletion or restoration Event when that flag changed.
2807
+ *
2808
+ * @param fields Shared System Event fields.
2809
+ * @param change Accepted Entity transition.
2810
+ * @returns Lifecycle Event draft, or `undefined`.
2811
+ */
1707
2812
  #deleteDraft(fields, change) {
1708
2813
  const previous = change.oldLifecycle ?? { archived: false, deleted: false };
1709
2814
  if (previous.deleted === change.lifecycle.deleted)
@@ -1731,6 +2836,12 @@ class EntityStateChangePublishing {
1731
2836
  }),
1732
2837
  };
1733
2838
  }
2839
+ /**
2840
+ * Schedules best-effort System Event publication and reports failure.
2841
+ *
2842
+ * @param runtime Context publication services.
2843
+ * @param event Packed System Event to publish.
2844
+ */
1734
2845
  #post(runtime, event) {
1735
2846
  try {
1736
2847
  // spine-log-boundary: server.repository_system_follow_up
@@ -1740,12 +2851,26 @@ class EntityStateChangePublishing {
1740
2851
  runtime.publisher.reportFailure("system-event", event, error);
1741
2852
  }
1742
2853
  }
2854
+ /**
2855
+ * Compares serialized state messages for a meaningful state change.
2856
+ *
2857
+ * @param schema Generated state schema.
2858
+ * @param left State before handling.
2859
+ * @param right State after handling.
2860
+ * @returns Whether their serialized bytes match.
2861
+ */
1743
2862
  #sameState(schema, left, right) {
1744
2863
  const leftBytes = toBinary(schema, left);
1745
2864
  const rightBytes = toBinary(schema, right);
1746
2865
  return (leftBytes.length === rightBytes.length &&
1747
2866
  leftBytes.every((value, index) => value === rightBytes[index]));
1748
2867
  }
2868
+ /**
2869
+ * Maps a repository Entity family to the System Event kind enum.
2870
+ *
2871
+ * @param kind Repository Entity family.
2872
+ * @returns Matching Protobuf Entity kind.
2873
+ */
1749
2874
  #kind(kind) {
1750
2875
  return kind === "aggregate"
1751
2876
  ? EntityOption_Kind.AGGREGATE
@@ -1753,6 +2878,13 @@ class EntityStateChangePublishing {
1753
2878
  ? EntityOption_Kind.PROJECTION
1754
2879
  : EntityOption_Kind.PROCESS_MANAGER;
1755
2880
  }
2881
+ /**
2882
+ * Packs an Entity identifier for System Event metadata.
2883
+ *
2884
+ * @param repository Repository carrying the ID schema.
2885
+ * @param entityId Changed Entity identifier.
2886
+ * @returns Packed identifier.
2887
+ */
1756
2888
  #packEntityId(repository, entityId) {
1757
2889
  return EntityIds.pack(repository.stateSchema, entityId);
1758
2890
  }
@@ -1762,6 +2894,14 @@ const EntityStateChangePublisher = Object.freeze(new EntityStateChangePublishing
1762
2894
  * Builds and best-effort dispatches accepted handler diagnostics.
1763
2895
  */
1764
2896
  class HandlerDispatchPublishing {
2897
+ /**
2898
+ * Publishes a best-effort CommandDispatchedToHandler diagnostic.
2899
+ *
2900
+ * @param runtime Context signal and publication services.
2901
+ * @param repository Repository receiving the Command.
2902
+ * @param command Source Command.
2903
+ * @param entityId Target Entity identifier.
2904
+ */
1765
2905
  command(runtime, repository, command, entityId) {
1766
2906
  try {
1767
2907
  const context = runtime.signalMetadata.eventContext({
@@ -1778,12 +2918,37 @@ class HandlerDispatchPublishing {
1778
2918
  runtime.publisher.reportFailure("event", create(EventSchema), error);
1779
2919
  }
1780
2920
  }
2921
+ /**
2922
+ * Publishes a best-effort EventDispatchedToSubscriber diagnostic.
2923
+ *
2924
+ * @param runtime Context signal and publication services.
2925
+ * @param repository Repository receiving the Event.
2926
+ * @param event Source Event.
2927
+ * @param entityId Target Entity identifier.
2928
+ */
1781
2929
  subscriber(runtime, repository, event, entityId) {
1782
2930
  this.#publishEvent(runtime, repository, event, entityId, EntityLog.EventDispatchedToSubscriberSchema);
1783
2931
  }
2932
+ /**
2933
+ * Publishes a best-effort EventDispatchedToReactor diagnostic.
2934
+ *
2935
+ * @param runtime Context signal and publication services.
2936
+ * @param repository Repository receiving the Event.
2937
+ * @param event Source Event.
2938
+ * @param entityId Target Entity identifier.
2939
+ */
1784
2940
  reactor(runtime, repository, event, entityId) {
1785
2941
  this.#publishEvent(runtime, repository, event, entityId, EntityLog.EventDispatchedToReactorSchema);
1786
2942
  }
2943
+ /**
2944
+ * Builds and publishes one subscriber or reactor dispatch diagnostic.
2945
+ *
2946
+ * @param runtime Context signal and publication services.
2947
+ * @param repository Repository receiving the Event.
2948
+ * @param event Source Event.
2949
+ * @param entityId Target Entity identifier.
2950
+ * @param schema Diagnostic message schema to emit.
2951
+ */
1787
2952
  #publishEvent(runtime, repository, event, entityId, schema) {
1788
2953
  try {
1789
2954
  const context = runtime.signalMetadata.eventContext({
@@ -1800,6 +2965,15 @@ class HandlerDispatchPublishing {
1800
2965
  runtime.publisher.reportFailure("event", create(EventSchema), error);
1801
2966
  }
1802
2967
  }
2968
+ /**
2969
+ * Packs a Command dispatch diagnostic with target and source metadata.
2970
+ *
2971
+ * @param repository Repository receiving the Command.
2972
+ * @param command Source Command.
2973
+ * @param entityId Target Entity identifier.
2974
+ * @param whenDispatched Dispatch timestamp, when available.
2975
+ * @returns Command dispatch diagnostic message.
2976
+ */
1803
2977
  #message(repository, command, entityId, whenDispatched) {
1804
2978
  return create(EntityLog.CommandDispatchedToHandlerSchema, {
1805
2979
  receiver: create(MessageIdSchema, {
@@ -1813,6 +2987,13 @@ class HandlerDispatchPublishing {
1813
2987
  }),
1814
2988
  });
1815
2989
  }
2990
+ /**
2991
+ * Registers and schedules a System Event diagnostic for publication.
2992
+ *
2993
+ * @param runtime Context publication services.
2994
+ * @param schema Diagnostic message schema.
2995
+ * @param event Packed diagnostic Event.
2996
+ */
1816
2997
  #post(runtime, schema, event) {
1817
2998
  try {
1818
2999
  runtime.registerSystemEventSchema(schema);
@@ -1823,9 +3004,26 @@ class HandlerDispatchPublishing {
1823
3004
  runtime.publisher.reportFailure("system-event", event, error);
1824
3005
  }
1825
3006
  }
3007
+ /**
3008
+ * Packs a target Entity identifier for dispatch diagnostics.
3009
+ *
3010
+ * @param repository Repository carrying the ID schema.
3011
+ * @param entityId Target Entity identifier.
3012
+ * @returns Packed identifier.
3013
+ */
1826
3014
  #packEntityId(repository, entityId) {
1827
3015
  return EntityIds.pack(repository.stateSchema, entityId);
1828
3016
  }
3017
+ /**
3018
+ * Packs a subscriber or reactor dispatch diagnostic message.
3019
+ *
3020
+ * @param schema Diagnostic message schema to emit.
3021
+ * @param repository Repository receiving the Event.
3022
+ * @param event Source Event.
3023
+ * @param entityId Target Entity identifier.
3024
+ * @param whenDispatched Dispatch timestamp, when available.
3025
+ * @returns Matching subscriber or reactor diagnostic message.
3026
+ */
1829
3027
  #eventMessage(schema, repository, event, entityId, whenDispatched) {
1830
3028
  const fields = {
1831
3029
  receiver: create(MessageIdSchema, {
@@ -1849,6 +3047,14 @@ Object.freeze(RepositorySignals);
1849
3047
  * Internal repository stand operations.
1850
3048
  */
1851
3049
  const RepositoryStand = {
3050
+ /**
3051
+ * Builds tenant, Version, and lifecycle options for a Stand update.
3052
+ *
3053
+ * @param tenantId Tenant receiving the update, when present.
3054
+ * @param version Committed Spine Version, when present.
3055
+ * @param lifecycle Accepted archived and deleted flags.
3056
+ * @returns Frozen Stand update options.
3057
+ */
1852
3058
  standUpdateOptions(tenantId, version, lifecycle) {
1853
3059
  return Object.freeze({
1854
3060
  ...(tenantId === undefined ? {} : { tenantId }),
@@ -1856,22 +3062,25 @@ const RepositoryStand = {
1856
3062
  lifecycle,
1857
3063
  });
1858
3064
  },
1859
- projectionVersion(version) {
1860
- return typeof version === "bigint" ? Number(version) : (version?.number ?? 0);
1861
- },
1862
- processManagerVersion(entity) {
1863
- const version = entity.version;
1864
- return typeof version === "number" ? version + 1 : 1;
1865
- },
1866
- processManagerProducedVersion(sequence) {
1867
- return sequence;
1868
- },
3065
+ /**
3066
+ * Maps one Event position to its Process Manager producer version.
3067
+ *
3068
+ * @param sequence One-based position among produced Events.
3069
+ * @returns Producer sequence number for Event metadata.
3070
+ */
1869
3071
  };
1870
3072
  Object.freeze(RepositoryStand);
1871
3073
  /**
1872
3074
  * Internal repository tenants operations.
1873
3075
  */
1874
3076
  const RepositoryTenants = {
3077
+ /**
3078
+ * Builds tenant-aware storage context for Entity inbox delivery.
3079
+ *
3080
+ * @param context Bounded context storage mode.
3081
+ * @param tenantId Delivery tenant when multitenant.
3082
+ * @returns Storage context for durable Entity inbox rows.
3083
+ */
1875
3084
  entityInboxDeliveryContext(context, tenantId) {
1876
3085
  if (!context.multitenant) {
1877
3086
  return Object.freeze({ name: context.name, multitenant: false });
@@ -1886,6 +3095,13 @@ const RepositoryTenants = {
1886
3095
  tenantId: RepositoryTenants.require(tid),
1887
3096
  });
1888
3097
  },
3098
+ /**
3099
+ * Builds tenant-aware storage context for Projection inbox delivery.
3100
+ *
3101
+ * @param context Bounded context storage mode.
3102
+ * @param tenantId Delivery tenant when multitenant.
3103
+ * @returns Storage context for durable Projection inbox rows.
3104
+ */
1889
3105
  projectionDeliveryContext(context, tenantId) {
1890
3106
  if (!context.multitenant) {
1891
3107
  return Object.freeze({ name: context.name, multitenant: false });
@@ -1900,6 +3116,13 @@ const RepositoryTenants = {
1900
3116
  tenantId: RepositoryTenants.require(tid),
1901
3117
  });
1902
3118
  },
3119
+ /**
3120
+ * Validates an Event tenant before Projection inbox handoff.
3121
+ *
3122
+ * @param context Bounded context storage mode.
3123
+ * @param event Event entering Projection delivery.
3124
+ * @returns Validated tenant, or `undefined` for single-tenant contexts.
3125
+ */
1903
3126
  requireProjectionTenant(context, event) {
1904
3127
  if (!context.multitenant) {
1905
3128
  return undefined;
@@ -1910,6 +3133,13 @@ const RepositoryTenants = {
1910
3133
  }
1911
3134
  return RepositoryTenants.require(tenantId);
1912
3135
  },
3136
+ /**
3137
+ * Validates an Event tenant before Process Manager inbox handoff.
3138
+ *
3139
+ * @param context Bounded context storage mode.
3140
+ * @param event Event entering Process Manager delivery.
3141
+ * @returns Validated tenant, or `undefined` for single-tenant contexts.
3142
+ */
1913
3143
  requirePmEventTenant(context, event) {
1914
3144
  if (!context.multitenant) {
1915
3145
  return undefined;
@@ -1920,6 +3150,13 @@ const RepositoryTenants = {
1920
3150
  }
1921
3151
  return RepositoryTenants.require(tenantId);
1922
3152
  },
3153
+ /**
3154
+ * Validates a Command tenant before Entity inbox handoff.
3155
+ *
3156
+ * @param context Bounded context storage mode.
3157
+ * @param command Command entering Entity delivery.
3158
+ * @returns Validated tenant, or `undefined` for single-tenant contexts.
3159
+ */
1923
3160
  requireCommandTenant(context, command) {
1924
3161
  if (!context.multitenant) {
1925
3162
  return undefined;
@@ -1930,6 +3167,13 @@ const RepositoryTenants = {
1930
3167
  }
1931
3168
  return RepositoryTenants.require(tenantId);
1932
3169
  },
3170
+ /**
3171
+ * Builds storage context from a Command actor tenant.
3172
+ *
3173
+ * @param context Bounded context storage mode.
3174
+ * @param command Command entering repository execution.
3175
+ * @returns Tenant-aware storage context.
3176
+ */
1933
3177
  storageContextForCommand(context, command) {
1934
3178
  if (!context.multitenant) {
1935
3179
  return Object.freeze({ name: context.name, multitenant: false });
@@ -1943,6 +3187,13 @@ const RepositoryTenants = {
1943
3187
  tenantId: RepositoryTenants.require(tenantId),
1944
3188
  });
1945
3189
  },
3190
+ /**
3191
+ * Builds storage context from an Event origin tenant.
3192
+ *
3193
+ * @param context Bounded context storage mode.
3194
+ * @param event Event entering repository execution.
3195
+ * @returns Tenant-aware storage context.
3196
+ */
1946
3197
  storageContextForEvent(context, event) {
1947
3198
  if (!context.multitenant) {
1948
3199
  return Object.freeze({ name: context.name, multitenant: false });
@@ -1956,6 +3207,13 @@ const RepositoryTenants = {
1956
3207
  tenantId: RepositoryTenants.require(tenantId),
1957
3208
  });
1958
3209
  },
3210
+ /**
3211
+ * Builds storage context from an explicit tenant value.
3212
+ *
3213
+ * @param context Bounded context storage mode.
3214
+ * @param tenantId Tenant to select when multitenant.
3215
+ * @returns Tenant-aware storage context.
3216
+ */
1959
3217
  storageContextForTenant(context, tenantId) {
1960
3218
  if (!context.multitenant)
1961
3219
  return Object.freeze({ name: context.name, multitenant: false });
@@ -1967,9 +3225,22 @@ const RepositoryTenants = {
1967
3225
  tenantId: RepositoryTenants.require(tenantId),
1968
3226
  });
1969
3227
  },
3228
+ /**
3229
+ * Reads a copied tenant identifier from Command actor metadata.
3230
+ *
3231
+ * @param command Source Command.
3232
+ * @returns Tenant identifier when present.
3233
+ */
1970
3234
  readCommandTenant(command) {
1971
3235
  return RepositoryTenants.tenantValue(command.context?.actorContext?.tenantId);
1972
3236
  },
3237
+ /**
3238
+ * Reads a tenant for a Stand Event update when available.
3239
+ *
3240
+ * @param context Bounded context storage mode.
3241
+ * @param event Source Event.
3242
+ * @returns Stand tenant options.
3243
+ */
1973
3244
  standTenantOptions(context, event) {
1974
3245
  if (!context.multitenant) {
1975
3246
  return {};
@@ -1977,6 +3248,13 @@ const RepositoryTenants = {
1977
3248
  const tenantId = RepositoryTenants.readEventTenant(event);
1978
3249
  return tenantId === undefined ? {} : { tenantId };
1979
3250
  },
3251
+ /**
3252
+ * Reads a tenant for a Stand Command update when available.
3253
+ *
3254
+ * @param context Bounded context storage mode.
3255
+ * @param command Source Command.
3256
+ * @returns Stand tenant options.
3257
+ */
1980
3258
  commandStandOptions(context, command) {
1981
3259
  if (!context.multitenant) {
1982
3260
  return {};
@@ -1984,6 +3262,12 @@ const RepositoryTenants = {
1984
3262
  const tenantId = RepositoryTenants.readCommandTenant(command);
1985
3263
  return tenantId === undefined ? {} : { tenantId };
1986
3264
  },
3265
+ /**
3266
+ * Reads a copied tenant identifier from Event origin metadata.
3267
+ *
3268
+ * @param event Source Event.
3269
+ * @returns Tenant identifier when present.
3270
+ */
1987
3271
  readEventTenant(event) {
1988
3272
  switch (event.context?.origin.case) {
1989
3273
  case "importContext":
@@ -1994,12 +3278,31 @@ const RepositoryTenants = {
1994
3278
  return undefined;
1995
3279
  }
1996
3280
  },
3281
+ /**
3282
+ * Copies a tenant identifier when provided.
3283
+ *
3284
+ * @param tenantId Tenant identifier to isolate from its source.
3285
+ * @returns Copied tenant, or `undefined`.
3286
+ */
1997
3287
  tenantValue(tenantId) {
1998
3288
  return tenantId === undefined ? undefined : clone(TenantIdSchema, tenantId);
1999
3289
  },
3290
+ /**
3291
+ * Validates and copies a tenant identifier through the storage boundary.
3292
+ *
3293
+ * @param tenantId Tenant identifier to validate.
3294
+ * @returns Validated tenant identifier.
3295
+ */
2000
3296
  require(tenantId) {
2001
3297
  return TenantBoundary.from(tenantId).tenantId;
2002
3298
  },
3299
+ /**
3300
+ * Compares normalized tenant storage keys.
3301
+ *
3302
+ * @param left First tenant identifier.
3303
+ * @param right Second tenant identifier.
3304
+ * @returns Whether both identifiers select the same tenant.
3305
+ */
2003
3306
  equal(left, right) {
2004
3307
  return TenantBoundary.from(left).key === TenantBoundary.from(right).key;
2005
3308
  },
@@ -2009,6 +3312,27 @@ Object.freeze(RepositoryTenants);
2009
3312
  * Internal repository handlers operations.
2010
3313
  */
2011
3314
  const RepositoryHandlers = {
3315
+ /**
3316
+ * Rejects outputs not declared by the invoked handler, even if a sibling declares them.
3317
+ *
3318
+ * @param handler Invoked handler declaration.
3319
+ * @param signals Returned domain messages to validate.
3320
+ */
3321
+ requireDeclaredOutputs(handler, signals) {
3322
+ const schemas = HandlerMetadataValues.returnedSchemas(handler);
3323
+ for (const signal of signals) {
3324
+ const typeName = EntityInvocation.messageTypeName(signal);
3325
+ if (!schemas.some((schema) => schema.typeName === typeName)) {
3326
+ throw new Error(`Handler "${handler.methodName}" returned undeclared message "${typeName}".`);
3327
+ }
3328
+ }
3329
+ },
3330
+ /**
3331
+ * Normalizes one or many handler metadata blocks to a frozen list.
3332
+ *
3333
+ * @param handlersOption Configured handler metadata.
3334
+ * @returns Frozen handler metadata list.
3335
+ */
2012
3336
  normalizeHandlers(handlersOption) {
2013
3337
  if (handlersOption === undefined) {
2014
3338
  return Object.freeze([]);
@@ -2018,17 +3342,28 @@ const RepositoryHandlers = {
2018
3342
  }
2019
3343
  return Object.freeze([handlersOption]);
2020
3344
  },
3345
+ /**
3346
+ * Groups Event-to-Command reactions by source Event type.
3347
+ *
3348
+ * @param handlers Entity handler metadata blocks.
3349
+ * @returns Reactions indexed by Event type name.
3350
+ */
2021
3351
  createCommandReactionMap(handlers) {
2022
3352
  const byEvent = new Map();
2023
3353
  const registry = new HandlerMetadataRegistry(handlers);
2024
3354
  for (const entry of registry.findHandlersByKind("command-reaction")) {
2025
- if (RepositoryHandlers.handlerEmittedSchemas(entry.handler).length === 0) {
2026
- continue;
2027
- }
2028
3355
  RepositoryHandlers.pushMapValue(byEvent, entry.handler.messageFullTypeName, entry);
2029
3356
  }
2030
3357
  return byEvent;
2031
3358
  },
3359
+ /**
3360
+ * Builds Event field filters for handlers grouped by Event type.
3361
+ *
3362
+ * @typeParam Value Handler registration carrying schema and filter metadata.
3363
+ * @param byEvent Handler registrations indexed by Event type.
3364
+ * @param stringifiers Field conversion registry for message-valued filters.
3365
+ * @returns Compiled filter plans by Event type.
3366
+ */
2032
3367
  createEventFilterPlans(byEvent, stringifiers) {
2033
3368
  const plans = new Map();
2034
3369
  for (const [eventType, values] of byEvent) {
@@ -2040,21 +3375,62 @@ const RepositoryHandlers = {
2040
3375
  }
2041
3376
  return plans;
2042
3377
  },
3378
+ /**
3379
+ * Finds handlers for domestic or external Event origin.
3380
+ *
3381
+ * @typeParam Value Handler registration with origin metadata.
3382
+ * @param values Candidate handler registrations.
3383
+ * @param external Whether the source Event is external.
3384
+ * @returns Frozen matching registration list.
3385
+ */
2043
3386
  forOrigin(values, external) {
2044
3387
  return Object.freeze(values.filter((value) => (value.handler.origin === "external") === external));
2045
3388
  },
3389
+ /**
3390
+ * Maps readiness-selected handlers by registered message type.
3391
+ *
3392
+ * @typeParam Value Handler registration returned by readiness lookup.
3393
+ * @param schemas Registered message schemas.
3394
+ * @param find Selects handlers for one type name.
3395
+ * @returns Handler lists indexed by message type name.
3396
+ */
2046
3397
  readinessMap(schemas, find) {
2047
3398
  return new Map(schemas.map((schema) => [schema.typeName, find(schema.typeName)]));
2048
3399
  },
3400
+ /**
3401
+ * Reads the declared output schemas for one handler.
3402
+ *
3403
+ * @param handler Command or Event handler declaration.
3404
+ * @returns Generated schemas the handler may emit.
3405
+ */
2049
3406
  handlerEmittedSchemas(handler) {
2050
3407
  return HandlerMetadataValues.returnedSchemas(handler);
2051
3408
  },
3409
+ /**
3410
+ * Reads the declared rejection schemas for a Command handler.
3411
+ *
3412
+ * @param handler Assignment or substitution declaration.
3413
+ * @returns Generated rejection schemas the handler may throw.
3414
+ */
2052
3415
  handlerThrownSchemas(handler) {
2053
3416
  return HandlerMetadataValues.thrownSchemas(handler);
2054
3417
  },
3418
+ /**
3419
+ * Checks whether handler metadata was supplied as a list.
3420
+ *
3421
+ * @param value Configured handler metadata.
3422
+ * @returns Whether it is a handler list.
3423
+ */
2055
3424
  isHandlersArray(value) {
2056
3425
  return Array.isArray(value);
2057
3426
  },
3427
+ /**
3428
+ * Validates handler metadata against the Entity constructor and family.
3429
+ *
3430
+ * @param entityType Repository Entity constructor.
3431
+ * @param metadata Descriptor-derived state metadata.
3432
+ * @param handlers Handler metadata blocks to validate.
3433
+ */
2058
3434
  validateHandlers(entityType, metadata, handlers) {
2059
3435
  for (const handlersMetadata of handlers) {
2060
3436
  if (!HandlerMetadataValues.isAuthentic(handlersMetadata) ||
@@ -2069,6 +3445,12 @@ const RepositoryHandlers = {
2069
3445
  }
2070
3446
  }
2071
3447
  },
3448
+ /**
3449
+ * Returns unique generated schemas by type URL.
3450
+ *
3451
+ * @param schemas Candidate message schemas.
3452
+ * @returns Frozen list retaining the last schema for each type URL.
3453
+ */
2072
3454
  uniqueSchemas(schemas) {
2073
3455
  const byTypeUrl = new Map();
2074
3456
  for (const schema of schemas) {
@@ -2076,6 +3458,15 @@ const RepositoryHandlers = {
2076
3458
  }
2077
3459
  return Object.freeze([...byTypeUrl.values()]);
2078
3460
  },
3461
+ /**
3462
+ * Adds a value to a map entry, creating its list when absent.
3463
+ *
3464
+ * @typeParam Key Map key type.
3465
+ * @typeParam Value Map value type.
3466
+ * @param map Mutable map of value lists.
3467
+ * @param key Entry key to update.
3468
+ * @param value Value to append.
3469
+ */
2079
3470
  pushMapValue(map, key, value) {
2080
3471
  const values = map.get(key);
2081
3472
  if (values === undefined) {
@@ -2090,6 +3481,21 @@ Object.freeze(RepositoryHandlers);
2090
3481
  * Internal repository routes operations.
2091
3482
  */
2092
3483
  const RepositoryRoutes = {
3484
+ /**
3485
+ * Builds immutable routing from an Entity constructor and its declarations.
3486
+ *
3487
+ * @typeParam EntityType Concrete repository Entity constructor.
3488
+ * @param entityType Entity constructor receiving signals.
3489
+ * @param entityFamily Aggregate, Projection, or Process Manager family.
3490
+ * @param metadata Descriptor-derived state metadata.
3491
+ * @param handlersOption Registered Entity handler metadata.
3492
+ * @param producedEvents Additional Event schemas this repository may emit.
3493
+ * @param commandRouting Captured Command route declarations.
3494
+ * @param eventRouting Captured Event route declarations.
3495
+ * @param stateUpdateRouting Captured state-update route declarations.
3496
+ * @param stringifiers Field conversion registry for Event filters.
3497
+ * @returns Immutable routing and handler selection operations.
3498
+ */
2093
3499
  createRepositoryRouting(entityType, entityFamily, metadata, handlersOption, producedEvents, commandRouting, eventRouting, stateUpdateRouting, stringifiers) {
2094
3500
  return RepositoryRoutes.prepareRouting({
2095
3501
  entityType,
@@ -2103,6 +3509,13 @@ const RepositoryRoutes = {
2103
3509
  stringifiers,
2104
3510
  });
2105
3511
  },
3512
+ /**
3513
+ * Validates handler declarations and assembles routing schemas and selectors.
3514
+ *
3515
+ * @typeParam Id Repository Entity identifier type.
3516
+ * @param input Entity metadata, handlers, and captured routes.
3517
+ * @returns Immutable repository routing.
3518
+ */
2106
3519
  prepareRouting(input) {
2107
3520
  const handlers = RepositoryHandlers.normalizeHandlers(input.handlersOption);
2108
3521
  RepositoryHandlers.validateHandlers(input.entityType, input.metadata, handlers);
@@ -2114,6 +3527,12 @@ const RepositoryRoutes = {
2114
3527
  const routes = RepositoryRoutes.routingMaps(schemas, input.commandRouting, input.eventRouting, input.stateUpdateRouting, input.metadata.idField);
2115
3528
  return RepositoryRoutes.freezeRouting(schemas, readiness, stateSubscriptions, filters, routes, input.metadata.idField);
2116
3529
  },
3530
+ /**
3531
+ * Builds Command and Event handler readiness lookups when handlers exist.
3532
+ *
3533
+ * @param handlers Registered Entity handler metadata.
3534
+ * @returns Readiness lookups, or absent lookups for no handlers.
3535
+ */
2117
3536
  readiness(handlers) {
2118
3537
  return handlers.length === 0
2119
3538
  ? { command: undefined, event: undefined }
@@ -2122,12 +3541,25 @@ const RepositoryRoutes = {
2122
3541
  event: EventRegistrationReadiness.fromEntityHandlers(handlers),
2123
3542
  };
2124
3543
  },
3544
+ /**
3545
+ * Collects accepted, produced, and origin-specific message schemas.
3546
+ *
3547
+ * @param handlers Registered Entity handler metadata.
3548
+ * @param producedEvents Additional Event schemas this repository may emit.
3549
+ * @returns Schema groups used to build routes.
3550
+ */
2125
3551
  routingSchemas(handlers, producedEvents) {
2126
3552
  const accepted = RepositoryRoutes.acceptedSchemas(handlers);
2127
3553
  const origins = RepositoryRoutes.originSchemas(handlers);
2128
3554
  const produced = RepositoryRoutes.producedSchemas(handlers, producedEvents);
2129
3555
  return { ...accepted, ...origins, ...produced };
2130
3556
  },
3557
+ /**
3558
+ * Collects Command, Event, and state schemas accepted by handlers.
3559
+ *
3560
+ * @param handlers Registered Entity handler metadata.
3561
+ * @returns Deduplicated accepted schema groups.
3562
+ */
2131
3563
  acceptedSchemas(handlers) {
2132
3564
  const command = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => [
2133
3565
  ...handler.commandAssignments.map((assignment) => assignment.schema),
@@ -2137,11 +3569,16 @@ const RepositoryRoutes = {
2137
3569
  ...handler.commandReactions.map((reaction) => reaction.schema),
2138
3570
  ...handler.eventSubscriptions.map((subscription) => subscription.schema),
2139
3571
  ...handler.eventReactions.map((reaction) => reaction.schema),
2140
- ...handler.eventApplications.map((application) => application.schema),
2141
3572
  ]));
2142
3573
  const state = RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.stateSubscriptions.map((subscription) => subscription.schema)));
2143
3574
  return { command, event, state };
2144
3575
  },
3576
+ /**
3577
+ * Groups domestic and external Event schemas declared by handlers.
3578
+ *
3579
+ * @param handlers Registered Entity handler metadata.
3580
+ * @returns Origin-specific Event schema groups.
3581
+ */
2145
3582
  originSchemas(handlers) {
2146
3583
  const schemas = (origin) => RepositoryHandlers.uniqueSchemas(handlers.flatMap((handler) => handler.handlers
2147
3584
  .filter((candidate) => candidate.origin === origin &&
@@ -2151,6 +3588,13 @@ const RepositoryRoutes = {
2151
3588
  .map((candidate) => candidate.schema)));
2152
3589
  return { domesticEvent: schemas("domestic"), externalEvent: schemas("external") };
2153
3590
  },
3591
+ /**
3592
+ * Collects schemas for Events and Commands handlers may produce.
3593
+ *
3594
+ * @param handlers Registered Entity handler metadata.
3595
+ * @param explicitEvents Additional Event schemas configured on the repository.
3596
+ * @returns Deduplicated produced Event and Command schema groups.
3597
+ */
2154
3598
  producedSchemas(handlers, explicitEvents) {
2155
3599
  const producedEvent = RepositoryHandlers.uniqueSchemas([
2156
3600
  ...explicitEvents,
@@ -2167,6 +3611,13 @@ const RepositoryRoutes = {
2167
3611
  ]));
2168
3612
  return { producedEvent, producedCommand };
2169
3613
  },
3614
+ /**
3615
+ * Rejects state subscriptions outside Projections or to the same state type.
3616
+ *
3617
+ * @param entityFamily Repository Entity family.
3618
+ * @param metadata Descriptor-derived state metadata.
3619
+ * @param schemas Subscribed Entity state schemas.
3620
+ */
2170
3621
  validateStateSchemas(entityFamily, metadata, schemas) {
2171
3622
  if (entityFamily !== "projection" && schemas.length > 0) {
2172
3623
  throw new Error("Entity state subscriptions are supported only by Projection repositories.");
@@ -2176,6 +3627,15 @@ const RepositoryRoutes = {
2176
3627
  "resulting update would be routed back to the same repository.");
2177
3628
  }
2178
3629
  },
3630
+ /**
3631
+ * Builds Event filters for reactors, subscribers, and Command reactions.
3632
+ *
3633
+ * @param handlers Registered Entity handler metadata.
3634
+ * @param eventSchemas Accepted Event schemas.
3635
+ * @param readiness Event handler readiness lookup, when present.
3636
+ * @param stringifiers Field conversion registry for filters.
3637
+ * @returns Compiled Event filter groups.
3638
+ */
2179
3639
  routingFilters(handlers, eventSchemas, readiness, stringifiers) {
2180
3640
  const commandReactionMap = RepositoryHandlers.createCommandReactionMap(handlers);
2181
3641
  const subscribers = RepositoryHandlers.readinessMap(eventSchemas, (typeName) => readiness?.findEventSubscribers(typeName) ?? []);
@@ -2187,6 +3647,17 @@ const RepositoryRoutes = {
2187
3647
  eventReactors: RepositoryHandlers.createEventFilterPlans(reactors, stringifiers),
2188
3648
  };
2189
3649
  },
3650
+ /**
3651
+ * Resolves captured custom routes for each accepted signal schema.
3652
+ *
3653
+ * @typeParam Id Repository Entity identifier type.
3654
+ * @param schemas Accepted signal and state schemas.
3655
+ * @param command Captured Command route declarations.
3656
+ * @param event Captured Event route declarations.
3657
+ * @param state Captured state-update route declarations.
3658
+ * @param idField Canonical target Entity ID field.
3659
+ * @returns Custom route maps by schema.
3660
+ */
2190
3661
  routingMaps(schemas, command, event, state, idField) {
2191
3662
  return {
2192
3663
  command: RepositoryRoutes.resolveCommandRoutes(schemas.command, command),
@@ -2194,6 +3665,18 @@ const RepositoryRoutes = {
2194
3665
  state: RepositoryRoutes.resolveStateRoutes(schemas.state, state, idField),
2195
3666
  };
2196
3667
  },
3668
+ /**
3669
+ * Creates immutable schema sets, handler selectors, and signal route operations.
3670
+ *
3671
+ * @typeParam Id Repository Entity identifier type.
3672
+ * @param schemas Accepted and produced schema groups.
3673
+ * @param readiness Handler readiness lookups.
3674
+ * @param stateSubscriptions State subscribers by source type.
3675
+ * @param filters Compiled Event filter groups.
3676
+ * @param routes Custom route maps by schema.
3677
+ * @param idField Canonical target Entity ID field.
3678
+ * @returns Immutable repository routing.
3679
+ */
2197
3680
  freezeRouting(schemas, readiness, stateSubscriptions, filters, routes, idField) {
2198
3681
  return Object.freeze({
2199
3682
  commandSchemas: schemas.command,
@@ -2210,6 +3693,12 @@ const RepositoryRoutes = {
2210
3693
  ...RepositoryRoutes.routeSelectors(schemas, readiness, stateSubscriptions, filters, routes, idField),
2211
3694
  });
2212
3695
  },
3696
+ /**
3697
+ * Builds filter-aware selectors for Command reactions and Event handlers.
3698
+ *
3699
+ * @param filters Compiled Event filter groups.
3700
+ * @returns Functions that select matching handler registrations.
3701
+ */
2213
3702
  handlerSelectors(filters) {
2214
3703
  return {
2215
3704
  commandReactions: (typeName, message, external) => RepositoryRoutes.selectHandlers(filters.commandReactions, typeName, message, external),
@@ -2217,9 +3706,31 @@ const RepositoryRoutes = {
2217
3706
  eventSubscribers: (typeName, message, external) => RepositoryRoutes.selectHandlers(filters.eventSubscribers, typeName, message, external),
2218
3707
  };
2219
3708
  },
3709
+ /**
3710
+ * Finds handlers matching message fields and Event origin.
3711
+ *
3712
+ * @typeParam Value Handler registration with origin metadata.
3713
+ * @param filters Compiled filter plans indexed by Event type.
3714
+ * @param typeName Source Event type name.
3715
+ * @param message Decoded Event message.
3716
+ * @param external Whether the Event has external origin.
3717
+ * @returns Matching handler registrations.
3718
+ */
2220
3719
  selectHandlers(filters, typeName, message, external) {
2221
3720
  return RepositoryHandlers.forOrigin(filters.get(typeName)?.select(message) ?? Object.freeze([]), external);
2222
3721
  },
3722
+ /**
3723
+ * Builds Command, Event, and state-update route functions.
3724
+ *
3725
+ * @typeParam Id Repository Entity identifier type.
3726
+ * @param schemas Accepted signal and state schemas.
3727
+ * @param readiness Handler readiness lookups.
3728
+ * @param stateSubscriptions State subscribers by source type.
3729
+ * @param filters Compiled Event filter groups.
3730
+ * @param routes Custom route maps by schema.
3731
+ * @param idField Canonical target Entity ID field.
3732
+ * @returns Route functions for repository dispatch.
3733
+ */
2223
3734
  routeSelectors(schemas, readiness, stateSubscriptions, filters, routes, idField) {
2224
3735
  return {
2225
3736
  routeCommand: RepositoryRoutes.commandSelector(schemas, readiness, routes, idField),
@@ -2227,15 +3738,57 @@ const RepositoryRoutes = {
2227
3738
  routeStateUpdate: RepositoryRoutes.stateSelector(schemas, stateSubscriptions, routes, idField),
2228
3739
  };
2229
3740
  },
3741
+ /**
3742
+ * Builds a Command route function from readiness and custom declarations.
3743
+ *
3744
+ * @typeParam Id Repository Entity identifier type.
3745
+ * @param schemas Accepted Command schemas.
3746
+ * @param readiness Command handler readiness lookup.
3747
+ * @param routes Custom route maps by schema.
3748
+ * @param idField Canonical target Entity ID field.
3749
+ * @returns Function that routes one Command.
3750
+ */
2230
3751
  commandSelector(schemas, readiness, routes, idField) {
2231
3752
  return (command) => RepositoryRoutes.routeCommand(command, readiness.command, schemas.command, idField, routes.command);
2232
3753
  },
3754
+ /**
3755
+ * Builds an Event route function from readiness and custom declarations.
3756
+ *
3757
+ * @typeParam Id Repository Entity identifier type.
3758
+ * @param schemas Accepted Event schemas.
3759
+ * @param readiness Event handler readiness lookup.
3760
+ * @param filters Compiled Event filter groups.
3761
+ * @param routes Custom route maps by schema.
3762
+ * @param idField Canonical target Entity ID field.
3763
+ * @returns Function that routes one Event.
3764
+ */
2233
3765
  eventSelector(schemas, readiness, filters, routes, idField) {
2234
3766
  return (event) => RepositoryRoutes.routeEvent(event, readiness.event, filters.commandReactionMap, schemas.event, idField, routes.event);
2235
3767
  },
3768
+ /**
3769
+ * Builds a state-update route function for Projection subscriptions.
3770
+ *
3771
+ * @typeParam Id Repository Entity identifier type.
3772
+ * @param schemas Subscribed state schemas.
3773
+ * @param subscriptions State subscribers by source type.
3774
+ * @param routes Custom state-update routes by schema.
3775
+ * @param idField Canonical target Entity ID field.
3776
+ * @returns Function that routes one state-change Event.
3777
+ */
2236
3778
  stateSelector(schemas, subscriptions, routes, idField) {
2237
3779
  return (event) => RepositoryRoutes.routeStateUpdate(event, schemas.state, subscriptions, idField, routes.state);
2238
3780
  },
3781
+ /**
3782
+ * Routes a Command to its registered assignee and target Entity ID.
3783
+ *
3784
+ * @typeParam Id Repository Entity identifier type.
3785
+ * @param command Command envelope to route.
3786
+ * @param readiness Command handler readiness lookup.
3787
+ * @param schemas Accepted Command schemas.
3788
+ * @param targetIdField Canonical target Entity ID field.
3789
+ * @param commandRoutes Custom Command routes by schema.
3790
+ * @returns Deferred Command route for one target.
3791
+ */
2239
3792
  routeCommand(command, readiness, schemas, targetIdField, commandRoutes) {
2240
3793
  const message = command.message;
2241
3794
  if (message === undefined || message.typeUrl === "") {
@@ -2256,6 +3809,14 @@ const RepositoryRoutes = {
2256
3809
  invocation: "deferred",
2257
3810
  });
2258
3811
  },
3812
+ /**
3813
+ * Resolves custom Command routes for registered schemas.
3814
+ *
3815
+ * @typeParam Id Repository Entity identifier type.
3816
+ * @param schemas Accepted Command schemas.
3817
+ * @param routing Captured Command route declarations.
3818
+ * @returns Custom Command routes by schema.
3819
+ */
2259
3820
  resolveCommandRoutes(schemas, routing) {
2260
3821
  const routes = new Map();
2261
3822
  RoutingDeclarations.validate(routing, schemas, "command");
@@ -2266,6 +3827,16 @@ const RepositoryRoutes = {
2266
3827
  }
2267
3828
  return routes;
2268
3829
  },
3830
+ /**
3831
+ * Calls a custom Command route with a decoded message and context.
3832
+ *
3833
+ * @typeParam Id Repository Entity identifier type.
3834
+ * @param route Registered custom route.
3835
+ * @param message Packed Command payload.
3836
+ * @param schema Registered Command schema.
3837
+ * @param context Source Command context, when present.
3838
+ * @returns Candidate target Entity ID.
3839
+ */
2269
3840
  callCommandRoute(route, message, schema, context) {
2270
3841
  const unpacked = AnyMessages.unpack(message, schema);
2271
3842
  if (unpacked === undefined) {
@@ -2273,34 +3844,56 @@ const RepositoryRoutes = {
2273
3844
  }
2274
3845
  return route(unpacked, context ?? create(CommandContextSchema));
2275
3846
  },
3847
+ /**
3848
+ * Routes an Event to interested Entity IDs using custom or default routing.
3849
+ *
3850
+ * @typeParam Id Repository Entity identifier type.
3851
+ * @param event Event envelope to route.
3852
+ * @param readiness Event handler readiness lookup.
3853
+ * @param commandReactions Command reactions by Event type.
3854
+ * @param schemas Accepted Event schemas.
3855
+ * @param targetIdField Canonical target Entity ID field.
3856
+ * @param eventRoutes Custom Event routes by schema.
3857
+ * @returns Deferred Event route with target IDs.
3858
+ */
2276
3859
  routeEvent(event, readiness, commandReactions, schemas, targetIdField, eventRoutes) {
2277
3860
  const message = event.message;
2278
- if (message === undefined || message.typeUrl === "") {
3861
+ if (message === undefined || message.typeUrl === "")
2279
3862
  throw new Error("Repository event routing requires event.message.typeUrl.");
2280
- }
2281
3863
  const schema = RepositoryRoutes.schemaForTypeUrl(schemas, message.typeUrl, "event");
2282
- const hasReceiver = (commandReactions.get(schema.typeName)?.length ?? 0) > 0 ||
2283
- (readiness?.findEventSubscribers(schema.typeName).length ?? 0) > 0 ||
2284
- (readiness?.findEventReactors(schema.typeName).length ?? 0) > 0 ||
2285
- (readiness?.findEventApplications(schema.typeName).length ?? 0) > 0;
2286
- if (!hasReceiver) {
3864
+ if (!RepositoryRoutes.hasEventReceiver(schema.typeName, commandReactions, readiness))
2287
3865
  throw new Error(`Repository event routing has no receiver for "${schema.typeName}".`);
2288
- }
2289
3866
  const customRoute = eventRoutes.get(schema);
2290
- if (customRoute !== undefined) {
2291
- return Object.freeze({
2292
- entityIds: RepositoryRoutes.callEventRoute(customRoute, message, schema, event.context, targetIdField),
2293
- messageFullTypeName: schema.typeName,
2294
- invocation: "deferred",
2295
- });
2296
- }
2297
- const targetId = RepositoryRoutes.readEventEntityId(event, message, schema, targetIdField);
3867
+ const entityIds = customRoute === undefined
3868
+ ? [RepositoryRoutes.readEventEntityId(event, message, schema, targetIdField)]
3869
+ : RepositoryRoutes.callEventRoute(customRoute, message, schema, event.context, targetIdField);
2298
3870
  return Object.freeze({
2299
- entityIds: Object.freeze([targetId]),
3871
+ entityIds: Object.freeze([...entityIds]),
2300
3872
  messageFullTypeName: schema.typeName,
2301
3873
  invocation: "deferred",
2302
3874
  });
2303
3875
  },
3876
+ /**
3877
+ * Checks whether any registered Event receptor accepts a message type.
3878
+ *
3879
+ * @param typeName Full generated Event type name.
3880
+ * @param reactions Command reactions registered by Event type.
3881
+ * @param readiness Event receptor readiness lookup.
3882
+ * @returns Whether at least one receptor accepts the type.
3883
+ */
3884
+ hasEventReceiver(typeName, reactions, readiness) {
3885
+ return ((reactions.get(typeName)?.length ?? 0) > 0 ||
3886
+ (readiness?.findEventSubscribers(typeName).length ?? 0) > 0 ||
3887
+ (readiness?.findEventReactors(typeName).length ?? 0) > 0);
3888
+ },
3889
+ /**
3890
+ * Resolves custom Event routes for registered schemas.
3891
+ *
3892
+ * @typeParam Id Repository Entity identifier type.
3893
+ * @param schemas Accepted Event schemas.
3894
+ * @param routing Captured Event route declarations.
3895
+ * @returns Custom Event routes by schema.
3896
+ */
2304
3897
  resolveEventRoutes(schemas, routing) {
2305
3898
  const routes = new Map();
2306
3899
  RoutingDeclarations.validate(routing, schemas, "event");
@@ -2311,6 +3904,12 @@ const RepositoryRoutes = {
2311
3904
  }
2312
3905
  return routes;
2313
3906
  },
3907
+ /**
3908
+ * Groups state subscribers by source Entity type name.
3909
+ *
3910
+ * @param handlers Registered Entity handler metadata.
3911
+ * @returns Frozen subscriber lists indexed by state type.
3912
+ */
2314
3913
  stateSubscriptions(handlers) {
2315
3914
  const byType = new Map();
2316
3915
  for (const entityHandlers of handlers) {
@@ -2325,6 +3924,15 @@ const RepositoryRoutes = {
2325
3924
  }
2326
3925
  return new Map([...byType].map(([typeName, subscriptions]) => [typeName, Object.freeze([...subscriptions])]));
2327
3926
  },
3927
+ /**
3928
+ * Resolves state-update routes and checks default ID compatibility.
3929
+ *
3930
+ * @typeParam Id Repository Entity identifier type.
3931
+ * @param schemas Subscribed source Entity state schemas.
3932
+ * @param routing Captured state-update route declarations.
3933
+ * @param targetIdField Canonical Projection ID field.
3934
+ * @returns Custom state-update routes by schema.
3935
+ */
2328
3936
  resolveStateRoutes(schemas, routing, targetIdField) {
2329
3937
  const routes = new Map();
2330
3938
  RoutingDeclarations.validate(routing, schemas, "state-update");
@@ -2338,13 +3946,23 @@ const RepositoryRoutes = {
2338
3946
  }
2339
3947
  return routes;
2340
3948
  },
3949
+ /**
3950
+ * Routes a System Event state update to interested Projections.
3951
+ *
3952
+ * @typeParam Id Projection identifier type.
3953
+ * @param event Entity state-change System Event.
3954
+ * @param schemas Subscribed source state schemas.
3955
+ * @param subscriptions State subscribers by source type.
3956
+ * @param targetIdField Canonical Projection ID field.
3957
+ * @param routes Custom state-update routes by schema.
3958
+ * @returns Route and decoded state, or `undefined` when uninterested.
3959
+ */
2341
3960
  routeStateUpdate(event, schemas, subscriptions, targetIdField, routes) {
2342
3961
  const update = RepositoryRoutes.decodeStateUpdate(event, schemas, "Repository state-update routing");
2343
3962
  const candidates = subscriptions.get(update?.schema.typeName ?? "") ?? [];
2344
- const interested = Object.freeze(candidates.filter((subscriber) => (subscriber.handler.origin === "external") === (event.context?.external === true)));
2345
- if (update === undefined || interested.length === 0) {
3963
+ const interested = RepositoryRoutes.originSubscribers(candidates, event);
3964
+ if (update === undefined || interested.length === 0)
2346
3965
  return undefined;
2347
- }
2348
3966
  const { schema, state } = update;
2349
3967
  const custom = routes.get(schema);
2350
3968
  const candidateIds = custom === undefined
@@ -2358,6 +3976,24 @@ const RepositoryRoutes = {
2358
3976
  invocation: "deferred",
2359
3977
  });
2360
3978
  },
3979
+ /**
3980
+ * Finds state subscribers whose origin declaration matches an Event.
3981
+ *
3982
+ * @param candidates Subscribers registered for one source state type.
3983
+ * @param event State-update Event carrying the domestic or external origin.
3984
+ * @returns Frozen matching subscriber list.
3985
+ */
3986
+ originSubscribers(candidates, event) {
3987
+ return Object.freeze(candidates.filter((subscriber) => (subscriber.handler.origin === "external") === (event.context?.external === true)));
3988
+ },
3989
+ /**
3990
+ * Decodes the new Entity state carried by a state-change System Event.
3991
+ *
3992
+ * @param event Candidate System Event.
3993
+ * @param schemas Subscribed source state schemas.
3994
+ * @param operation Operation name for diagnostics.
3995
+ * @returns Decoded schema and state, or `undefined` for unrelated Events.
3996
+ */
2361
3997
  decodeStateUpdate(event, schemas, operation) {
2362
3998
  const message = event.message;
2363
3999
  if (message?.typeUrl !== TypeUrls.derive(EntityLog.EntityStateChangedSchema)) {
@@ -2376,6 +4012,16 @@ const RepositoryRoutes = {
2376
4012
  throw new Error(`${operation} requires a readable Entity state.`);
2377
4013
  return Object.freeze({ schema, state });
2378
4014
  },
4015
+ /**
4016
+ * Calls a custom state-update route and validates its target IDs.
4017
+ *
4018
+ * @typeParam Id Projection identifier type.
4019
+ * @param route Registered state-update route.
4020
+ * @param state Decoded source Entity state.
4021
+ * @param context Source Event context, when present.
4022
+ * @param targetIdField Canonical Projection ID field.
4023
+ * @returns Validated Projection target IDs.
4024
+ */
2379
4025
  callStateUpdateRoute(route, state, context, targetIdField) {
2380
4026
  const candidates = route(state, context ?? create(EventContextSchema));
2381
4027
  if (!Array.isArray(candidates))
@@ -2391,6 +4037,17 @@ const RepositoryRoutes = {
2391
4037
  }
2392
4038
  return Object.freeze([...unique.values()]);
2393
4039
  },
4040
+ /**
4041
+ * Calls a custom Event route and validates its target IDs.
4042
+ *
4043
+ * @typeParam Id Repository Entity identifier type.
4044
+ * @param route Registered custom Event route.
4045
+ * @param message Packed Event payload.
4046
+ * @param schema Registered Event schema.
4047
+ * @param context Source Event context, when present.
4048
+ * @param targetIdField Canonical target Entity ID field.
4049
+ * @returns Validated target Entity IDs.
4050
+ */
2394
4051
  callEventRoute(route, message, schema, context, targetIdField) {
2395
4052
  const unpacked = AnyMessages.unpack(message, schema);
2396
4053
  if (unpacked === undefined) {
@@ -2412,6 +4069,14 @@ const RepositoryRoutes = {
2412
4069
  }
2413
4070
  return Object.freeze([...unique.values()]);
2414
4071
  },
4072
+ /**
4073
+ * Finds a registered message schema by its type URL.
4074
+ *
4075
+ * @param schemas Accepted message schemas.
4076
+ * @param typeUrl Packed message type URL.
4077
+ * @param signalKind Signal name for diagnostics.
4078
+ * @returns Matching generated schema.
4079
+ */
2415
4080
  schemaForTypeUrl(schemas, typeUrl, signalKind) {
2416
4081
  const schema = schemas.find((candidate) => TypeUrls.derive(candidate) === typeUrl);
2417
4082
  if (schema === undefined) {
@@ -2419,6 +4084,14 @@ const RepositoryRoutes = {
2419
4084
  }
2420
4085
  return schema;
2421
4086
  },
4087
+ /**
4088
+ * Reads the default target ID from a decoded signal's first field.
4089
+ *
4090
+ * @param message Packed Command or Event payload.
4091
+ * @param schema Generated payload schema.
4092
+ * @param signalKind Signal name for validation.
4093
+ * @returns Nonempty first-field value.
4094
+ */
2422
4095
  readFirstFieldId(message, schema, signalKind) {
2423
4096
  const unpacked = AnyMessages.unpack(message, schema);
2424
4097
  const firstField = schema.fields[0];
@@ -2437,6 +4110,15 @@ const RepositoryRoutes = {
2437
4110
  }
2438
4111
  return value;
2439
4112
  },
4113
+ /**
4114
+ * Reads the first state field compatible with a Projection target ID.
4115
+ *
4116
+ * @param state Decoded source Entity state.
4117
+ * @param schema Source state schema.
4118
+ * @param targetIdField Canonical Projection ID field.
4119
+ * @param signalKind State-update name for validation.
4120
+ * @returns Validated Projection target ID.
4121
+ */
2440
4122
  firstCompatibleId(state, schema, targetIdField, signalKind) {
2441
4123
  const field = schema.fields.find((candidate) => RepositoryRoutes.compatibleStateIdField(candidate, targetIdField));
2442
4124
  if (field === undefined)
@@ -2444,6 +4126,13 @@ const RepositoryRoutes = {
2444
4126
  const value = state[field.localName];
2445
4127
  return RepositoryRoutes.readRouteId(value, targetIdField, signalKind);
2446
4128
  },
4129
+ /**
4130
+ * Checks whether a source state field can route to the target ID type.
4131
+ *
4132
+ * @param field Candidate source state field.
4133
+ * @param targetIdField Canonical target Entity ID field.
4134
+ * @returns Whether message or primitive ID types match.
4135
+ */
2447
4136
  compatibleStateIdField(field, targetIdField) {
2448
4137
  if (field.fieldKind === "list" || field.fieldKind === "map")
2449
4138
  return false;
@@ -2460,10 +4149,25 @@ const RepositoryRoutes = {
2460
4149
  return false;
2461
4150
  }
2462
4151
  },
4152
+ /**
4153
+ * Checks whether a candidate route ID is blank text.
4154
+ *
4155
+ * @param value Candidate identifier value.
4156
+ * @returns Whether the value is an empty or whitespace string.
4157
+ */
2463
4158
  isBlankRouteId(value) {
2464
4159
  const id = PrimitiveIds.readFinite(value);
2465
4160
  return typeof id === "string" && id.trim().length === 0;
2466
4161
  },
4162
+ /**
4163
+ * Reads a compatible producer ID or the Event payload's first-field ID.
4164
+ *
4165
+ * @param event Source Event envelope.
4166
+ * @param message Packed Event payload.
4167
+ * @param schema Generated Event schema.
4168
+ * @param targetIdField Canonical target Entity ID field.
4169
+ * @returns Validated target Entity ID.
4170
+ */
2467
4171
  readEventEntityId(event, message, schema, targetIdField) {
2468
4172
  const producerId = event.context?.producerId;
2469
4173
  if (producerId === undefined || producerId.typeUrl.trim().length === 0) {
@@ -2475,6 +4179,13 @@ const RepositoryRoutes = {
2475
4179
  }
2476
4180
  return RepositoryRoutes.readRouteId(RepositoryRoutes.readFirstFieldId(message, schema, "event"), targetIdField, "event");
2477
4181
  },
4182
+ /**
4183
+ * Unpacks a producer ID when its type matches the target ID field.
4184
+ *
4185
+ * @param producerId Packed source producer identifier.
4186
+ * @param targetIdField Canonical target Entity ID field.
4187
+ * @returns Compatible decoded ID, or an incompatible marker.
4188
+ */
2478
4189
  compatibleProducerId(producerId, targetIdField) {
2479
4190
  const descriptor = targetIdField.descriptor;
2480
4191
  if (descriptor.fieldKind === "message") {
@@ -2482,9 +4193,8 @@ const RepositoryRoutes = {
2482
4193
  if (producerId.typeUrl !== TypeUrls.derive(schema))
2483
4194
  return { compatible: false };
2484
4195
  const id = Identifiers.unpack(schema, producerId);
2485
- if (id === undefined) {
4196
+ if (id === undefined)
2486
4197
  throw new Error("Repository event routing requires a readable compatible producer ID.");
2487
- }
2488
4198
  return { compatible: true, id };
2489
4199
  }
2490
4200
  if (descriptor.fieldKind !== "scalar") {
@@ -2503,11 +4213,16 @@ const RepositoryRoutes = {
2503
4213
  : type === "int32"
2504
4214
  ? Identifiers.unpack("int32", producerId)
2505
4215
  : Identifiers.unpack("int64", producerId);
2506
- if (id === undefined) {
4216
+ if (id === undefined)
2507
4217
  throw new Error("Repository event routing requires a readable compatible producer ID.");
2508
- }
2509
4218
  return { compatible: true, id };
2510
4219
  },
4220
+ /**
4221
+ * Maps a supported Protobuf scalar to its primitive identifier family.
4222
+ *
4223
+ * @param type Protobuf scalar field type.
4224
+ * @returns String, 32-bit integer, or 64-bit integer ID family.
4225
+ */
2511
4226
  primitiveIdentifierType(type) {
2512
4227
  switch (type) {
2513
4228
  case ScalarType.STRING:
@@ -2524,6 +4239,14 @@ const RepositoryRoutes = {
2524
4239
  throw new Error("Repository event routing requires a supported Entity ID type.");
2525
4240
  }
2526
4241
  },
4242
+ /**
4243
+ * Validates a candidate target ID against the Entity state ID field.
4244
+ *
4245
+ * @param value Candidate identifier.
4246
+ * @param targetIdField Canonical target Entity ID field.
4247
+ * @param signalKind Signal name for diagnostics.
4248
+ * @returns Validated message or primitive identifier.
4249
+ */
2527
4250
  readRouteId(value, targetIdField, signalKind) {
2528
4251
  const descriptor = targetIdField.descriptor;
2529
4252
  if (descriptor.fieldKind === "message") {
@@ -2534,6 +4257,14 @@ const RepositoryRoutes = {
2534
4257
  }
2535
4258
  throw new Error(`Repository ${signalKind} routing requires a scalar or message-valued ID.`);
2536
4259
  },
4260
+ /**
4261
+ * Validates a generated message-valued target ID.
4262
+ *
4263
+ * @param value Candidate identifier.
4264
+ * @param targetSchema Generated target ID schema.
4265
+ * @param signalKind Signal name for diagnostics.
4266
+ * @returns Validated and encodable ID message.
4267
+ */
2537
4268
  readMessageRouteId(value, targetSchema, signalKind) {
2538
4269
  const id = MessageIds.read(value);
2539
4270
  if (id === undefined) {
@@ -2556,6 +4287,14 @@ const RepositoryRoutes = {
2556
4287
  }
2557
4288
  return id;
2558
4289
  },
4290
+ /**
4291
+ * Validates a primitive target ID against a Protobuf scalar type.
4292
+ *
4293
+ * @param value Candidate identifier.
4294
+ * @param targetType Canonical ID field scalar type.
4295
+ * @param signalKind Signal name for diagnostics.
4296
+ * @returns Compatible string, number, or bigint ID.
4297
+ */
2559
4298
  readPrimitiveRouteId(value, targetType, signalKind) {
2560
4299
  const id = RepositoryRoutes.readCompatiblePrimitiveId(value, targetType);
2561
4300
  if (id === undefined) {
@@ -2563,6 +4302,13 @@ const RepositoryRoutes = {
2563
4302
  }
2564
4303
  return id;
2565
4304
  },
4305
+ /**
4306
+ * Reads a nonblank primitive ID within the target scalar's range.
4307
+ *
4308
+ * @param value Candidate identifier.
4309
+ * @param targetType Canonical ID field scalar type.
4310
+ * @returns Compatible primitive ID, or `undefined`.
4311
+ */
2566
4312
  readCompatiblePrimitiveId(value, targetType) {
2567
4313
  switch (targetType) {
2568
4314
  case ScalarType.STRING:
@@ -2592,6 +4338,15 @@ Object.freeze(RepositoryRoutes);
2592
4338
  * Internal repository storage operations.
2593
4339
  */
2594
4340
  const RepositoryStorage = {
4341
+ /**
4342
+ * Opens Entity storage and an atomic commit port from a storage factory.
4343
+ *
4344
+ * @typeParam I Entity identifier type.
4345
+ * @typeParam S Generated Entity state type.
4346
+ * @param factory Provider storage factory.
4347
+ * @param input Entity storage location and schema.
4348
+ * @returns Storage ports closed together by one handle.
4349
+ */
2595
4350
  openEntityStorage(factory, input) {
2596
4351
  const candidate = factory;
2597
4352
  if (candidate.createEntityStorage === undefined) {
@@ -2608,6 +4363,16 @@ const RepositoryStorage = {
2608
4363
  },
2609
4364
  };
2610
4365
  },
4366
+ /**
4367
+ * Returns one shared Entity storage handle per context and state type.
4368
+ *
4369
+ * @typeParam I Entity identifier type.
4370
+ * @typeParam S Generated Entity state type.
4371
+ * @param repository Repository tracking open handles.
4372
+ * @param factory Provider storage factory.
4373
+ * @param input Entity storage location and schema.
4374
+ * @returns Shared Entity storage handle.
4375
+ */
2611
4376
  openRepositoryEntityStorage(repository, factory, input) {
2612
4377
  const handle = RepositoryStorage.openEntityStorage(factory, input);
2613
4378
  const key = JSON.stringify({ context: input.context, state: input.sourceType.typeName });
@@ -2624,6 +4389,13 @@ const RepositoryStorage = {
2624
4389
  handles.set(key, handle);
2625
4390
  return handle;
2626
4391
  },
4392
+ /**
4393
+ * Builds Entity storage input with configured state and Event history flags.
4394
+ *
4395
+ * @param repository Repository Entity registration.
4396
+ * @param context Tenant-aware storage context.
4397
+ * @returns Entity storage description for the provider.
4398
+ */
2627
4399
  entityStorageInput(repository, context) {
2628
4400
  const descriptor = entityStorageDescriptor(context, SpecScanner.scan(repository.entityType));
2629
4401
  const history = RepositoryStorage.historyConfiguration(repository);
@@ -2633,6 +4405,13 @@ const RepositoryStorage = {
2633
4405
  eventHistory: repository.entityFamily === "aggregate" || history.processManagerEventHistory,
2634
4406
  };
2635
4407
  },
4408
+ /**
4409
+ * Validates history and duplicate-dispatch options for an Entity family.
4410
+ *
4411
+ * @param options Repository history and guard settings.
4412
+ * @param family Aggregate, Projection, or Process Manager family.
4413
+ * @returns Normalized history and guard configuration.
4414
+ */
2636
4415
  readHistoryConfiguration(options, family) {
2637
4416
  const guard = options.doubleDispatchGuard;
2638
4417
  const depth = guard === true
@@ -2657,22 +4436,32 @@ const RepositoryStorage = {
2657
4436
  dispatchGuardDepth: depth,
2658
4437
  };
2659
4438
  },
4439
+ /**
4440
+ * Reads history and guard settings captured for a repository.
4441
+ *
4442
+ * @param repository Repository to inspect.
4443
+ * @returns Its validated configuration.
4444
+ */
2660
4445
  historyConfiguration(repository) {
2661
4446
  const configuration = repositoryHistoryConfigurations.get(repository);
2662
4447
  if (configuration === undefined)
2663
4448
  throw new Error("Repository history configuration is unavailable.");
2664
4449
  return configuration;
2665
4450
  },
2666
- /**
2667
- * In-process bounded duplicate guard. Completion is recorded only after a successful execution;
2668
- * therefore provider/journal failures may be retried and separate machines remain independent.
2669
- */
2670
4451
  };
2671
4452
  Object.freeze(RepositoryStorage);
2672
4453
  /**
2673
4454
  * Internal repository history internals operations.
2674
4455
  */
2675
4456
  const RepositoryHistoryInternals = {
4457
+ /**
4458
+ * Binds state and Event history readers to one restored Entity instance.
4459
+ *
4460
+ * @param entity Instance receiving the history accessors.
4461
+ * @param storage Entity storage used for reads and maintenance.
4462
+ * @param entityId Identifier used to select history records.
4463
+ * @param schema Schema used to unpack state records.
4464
+ */
2676
4465
  bindEntityHistory(entity, storage, entityId, schema) {
2677
4466
  const stateCache = RepositoryHistoryInternals.createHistoryCache((depth, startingFromVersion) => storage.states.backward(entityId, depth, startingFromVersion), (record) => BigInt(EntityRecords.unpack(schema, record).versionMessage.number), { requireDescendingVersions: true });
2678
4467
  entityStateHistoryCaches.set(entity, stateCache);
@@ -2686,24 +4475,52 @@ const RepositoryHistoryInternals = {
2686
4475
  });
2687
4476
  },
2688
4477
  /**
2689
- * Creates a per-live-entity continuation cache that serializes a larger request behind a prior read.
4478
+ * Creates a history cache that serializes reads and extends prior results.
2690
4479
  *
4480
+ * @typeParam T State-record or Event type retained by the cache.
4481
+ * @param load Reads a backward page, optionally continuing from a version.
4482
+ * @param versionOf Reads the version used to continue a page.
4483
+ * @param options State ordering and complete Event-group cache rules.
4484
+ * @returns Read and clear operations for the new cache.
2691
4485
  * @internal Shared repository history-cache implementation, exercised by repository tests.
2692
4486
  */
2693
4487
  createHistoryCache(load, versionOf, options = {}) {
2694
4488
  return new RepositoryHistoryCache(load, versionOf, options).api();
2695
4489
  },
4490
+ /**
4491
+ * Copies a historical state and freezes the returned message object.
4492
+ *
4493
+ * @param state Historical state, absent when no record matched.
4494
+ * @returns An independent state copy, or undefined.
4495
+ */
2696
4496
  cloneHistoryState(state) {
2697
4497
  return state === undefined ? undefined : Object.freeze(structuredClone(state));
2698
4498
  },
4499
+ /**
4500
+ * Copies historical states and freezes their outer objects and list.
4501
+ *
4502
+ * @param states Historical states read from storage.
4503
+ * @returns A frozen list of independent state copies.
4504
+ */
2699
4505
  freezeHistoryStates(states) {
2700
4506
  return Object.freeze(states.map((state) => Object.freeze(structuredClone(state))));
2701
4507
  },
4508
+ /**
4509
+ * Copies historical Events and freezes their outer objects and list.
4510
+ *
4511
+ * @param events Historical Events read from storage.
4512
+ * @returns A frozen list of independent Event copies.
4513
+ */
2702
4514
  freezeHistoryEvents(events) {
2703
4515
  return Object.freeze(events.map((event) => Object.freeze(clone(EventSchema, event))));
2704
4516
  },
2705
4517
  };
2706
4518
  Object.freeze(RepositoryHistoryInternals);
4519
+ /**
4520
+ * Reuses backward history pages during the lifetime of a restored Entity.
4521
+ *
4522
+ * @typeParam T State-record or Event type returned by the history reader.
4523
+ */
2707
4524
  class RepositoryHistoryCache {
2708
4525
  #load;
2709
4526
  #versionOf;
@@ -2715,12 +4532,24 @@ class RepositoryHistoryCache {
2715
4532
  #nextVersion;
2716
4533
  #newestVersion;
2717
4534
  #generation = 0;
4535
+ /**
4536
+ * Creates an empty cache with the supplied history paging rules.
4537
+ *
4538
+ * @param load Reads a backward page, optionally continuing from a version.
4539
+ * @param versionOf Extracts the continuation version from a history entry.
4540
+ * @param options State ordering and complete Event-group cache rules.
4541
+ */
2718
4542
  constructor(load, versionOf, options) {
2719
4543
  this.#load = load;
2720
4544
  this.#versionOf = versionOf;
2721
4545
  this.#requireDescendingVersions = options.requireDescendingVersions ?? false;
2722
4546
  this.#cacheCompleteVersionGroups = options.cacheCompleteVersionGroups ?? false;
2723
4547
  }
4548
+ /**
4549
+ * Exposes bound read and clear callbacks without exposing cache fields.
4550
+ *
4551
+ * @returns The operations used by Entity history accessors.
4552
+ */
2724
4553
  api() {
2725
4554
  return Object.freeze({
2726
4555
  read: (depth) => this.read(depth),
@@ -2729,6 +4558,9 @@ class RepositoryHistoryCache {
2729
4558
  },
2730
4559
  });
2731
4560
  }
4561
+ /**
4562
+ * Clears cached entries and prevents pending reads from restoring stale pages.
4563
+ */
2732
4564
  clear() {
2733
4565
  this.#generation += 1;
2734
4566
  this.#entries = Object.freeze([]);
@@ -2736,6 +4568,12 @@ class RepositoryHistoryCache {
2736
4568
  this.#nextVersion = undefined;
2737
4569
  this.#newestVersion = undefined;
2738
4570
  }
4571
+ /**
4572
+ * Reads up to the requested number of entries, extending cached history as needed.
4573
+ *
4574
+ * @param depth Maximum number of entries to return, newest first.
4575
+ * @returns Cached and newly loaded entries, limited to the requested depth.
4576
+ */
2739
4577
  async read(depth) {
2740
4578
  await this.#continuation;
2741
4579
  if (this.#isSatisfied(depth))
@@ -2751,9 +4589,21 @@ class RepositoryHistoryCache {
2751
4589
  await next;
2752
4590
  return result ?? this.#entries.slice(0, depth);
2753
4591
  }
4592
+ /**
4593
+ * Checks whether cached entries or known exhaustion satisfy a read.
4594
+ *
4595
+ * @param depth Requested number of entries.
4596
+ * @returns True when another storage read is unnecessary.
4597
+ */
2754
4598
  #isSatisfied(depth) {
2755
4599
  return this.#entries.length >= depth || this.#exhausted;
2756
4600
  }
4601
+ /**
4602
+ * Loads the missing entries using the configured continuation rule.
4603
+ *
4604
+ * @param depth Total number of entries requested by the caller.
4605
+ * @returns A result containing uncached partial groups, or undefined to use the cache.
4606
+ */
2757
4607
  async #extend(depth) {
2758
4608
  const generation = this.#generation;
2759
4609
  const requested = Math.max(1, depth - this.#entries.length);
@@ -2761,6 +4611,14 @@ class RepositoryHistoryCache {
2761
4611
  ? this.#extendCompleteGroups(depth, requested, generation)
2762
4612
  : this.#extendVersioned(depth, requested, generation);
2763
4613
  }
4614
+ /**
4615
+ * Loads an extra entry so a partial terminal version group is not cached.
4616
+ *
4617
+ * @param depth Total number of entries requested by the caller.
4618
+ * @param requested Number of missing entries, excluding the look-ahead entry.
4619
+ * @param generation Cache generation at the start of this read.
4620
+ * @returns Requested entries, or undefined if the cache was cleared during loading.
4621
+ */
2764
4622
  async #extendCompleteGroups(depth, requested, generation) {
2765
4623
  const loaded = await this.#load(requested + 1, this.#nextVersion);
2766
4624
  if (generation !== this.#generation)
@@ -2771,6 +4629,13 @@ class RepositoryHistoryCache {
2771
4629
  this.#exhausted = loaded.length < requested + 1;
2772
4630
  return Object.freeze(combined.slice(0, depth));
2773
4631
  }
4632
+ /**
4633
+ * Removes a terminal version group when the page may contain only part of it.
4634
+ *
4635
+ * @param loaded Page including the look-ahead entry when available.
4636
+ * @param requested Number requested before adding the look-ahead entry.
4637
+ * @returns Entries whose version groups are complete in this page.
4638
+ */
2774
4639
  #completeGroups(loaded, requested) {
2775
4640
  const terminal = loaded.at(-1);
2776
4641
  const terminalVersion = terminal === undefined ? undefined : this.#versionOf(terminal);
@@ -2779,6 +4644,14 @@ class RepositoryHistoryCache {
2779
4644
  : loaded.findIndex((entry) => this.#versionOf(entry) === terminalVersion);
2780
4645
  return loaded.slice(0, Math.max(0, length));
2781
4646
  }
4647
+ /**
4648
+ * Loads more version-ordered history, refreshing or clearing invalid continuations.
4649
+ *
4650
+ * @param depth Total number of entries requested by the caller.
4651
+ * @param requested Number of missing entries to load.
4652
+ * @param generation Cache generation at the start of this read.
4653
+ * @returns Undefined because accepted entries are read from the updated cache.
4654
+ */
2782
4655
  async #extendVersioned(depth, requested, generation) {
2783
4656
  const loaded = await this.#load(requested, this.#nextVersion);
2784
4657
  if (generation !== this.#generation)
@@ -2799,6 +4672,12 @@ class RepositoryHistoryCache {
2799
4672
  this.#exhausted = loaded.length < requested;
2800
4673
  return undefined;
2801
4674
  }
4675
+ /**
4676
+ * Checks strict descending versions when required for state history.
4677
+ *
4678
+ * @param loaded Page to inspect for repeated or increasing versions.
4679
+ * @returns True when two readable adjacent versions violate the ordering rule.
4680
+ */
2802
4681
  #hasInvalidPage(loaded) {
2803
4682
  if (!this.#requireDescendingVersions)
2804
4683
  return false;
@@ -2810,12 +4689,24 @@ class RepositoryHistoryCache {
2810
4689
  }
2811
4690
  return false;
2812
4691
  }
4692
+ /**
4693
+ * Checks whether a continuation page is newer than the newest cached entry.
4694
+ *
4695
+ * @param latest First version in the loaded page, if readable.
4696
+ * @returns True when the cache must restart from the newest history.
4697
+ */
2813
4698
  #hasNewerPage(latest) {
2814
4699
  return (this.#newestVersion !== undefined &&
2815
4700
  latest !== undefined &&
2816
4701
  latest > this.#newestVersion &&
2817
4702
  this.#nextVersion !== undefined);
2818
4703
  }
4704
+ /**
4705
+ * Checks that a state-history continuation starts below the oldest cached version.
4706
+ *
4707
+ * @param latest First version in the loaded page, if readable.
4708
+ * @returns True when the continuation overlaps or precedes the cached boundary.
4709
+ */
2819
4710
  #hasInvalidContinuation(latest) {
2820
4711
  const oldest = this.#entries.at(-1);
2821
4712
  const oldestVersion = oldest === undefined ? undefined : this.#versionOf(oldest);
@@ -2824,6 +4715,13 @@ class RepositoryHistoryCache {
2824
4715
  latest !== undefined &&
2825
4716
  latest >= oldestVersion);
2826
4717
  }
4718
+ /**
4719
+ * Reads history again from the newest entry after detecting a newer page.
4720
+ *
4721
+ * @param depth Maximum number of entries to reload.
4722
+ * @param generation Cache generation before this refresh clears the cache.
4723
+ * @returns Undefined because accepted entries are stored in the cache.
4724
+ */
2827
4725
  async #refresh(depth, generation) {
2828
4726
  this.clear();
2829
4727
  const refreshed = await this.#load(depth);
@@ -2836,6 +4734,11 @@ class RepositoryHistoryCache {
2836
4734
  this.#exhausted = refreshed.length < depth;
2837
4735
  return undefined;
2838
4736
  }
4737
+ /**
4738
+ * Adds an accepted page and records its oldest readable version.
4739
+ *
4740
+ * @param loaded Entries to add in storage order.
4741
+ */
2839
4742
  #append(loaded) {
2840
4743
  this.#entries = Object.freeze([...this.#entries, ...loaded]);
2841
4744
  const last = loaded.at(-1);
@@ -2843,9 +4746,19 @@ class RepositoryHistoryCache {
2843
4746
  }
2844
4747
  }
2845
4748
  /**
2846
- * Internal dispatch guards operations.
4749
+ * Serializes guarded Event delivery and remembers recent completed dispatches.
2847
4750
  */
2848
4751
  const DispatchGuards = {
4752
+ /**
4753
+ * Dispatches an Event with duplicate checks when the repository enables them.
4754
+ *
4755
+ * @param repository Repository receiving the Event.
4756
+ * @param runtime Storage and tenant context for the dispatch.
4757
+ * @param event Event being delivered.
4758
+ * @param entityId Target Entity identifier.
4759
+ * @param dispatch Operation to run unless a completed delivery is found.
4760
+ * @returns Completion of the dispatch or duplicate check.
4761
+ */
2849
4762
  async guardedEntityEventDispatch(repository, runtime, event, entityId, dispatch) {
2850
4763
  const depth = RepositoryStorage.historyConfiguration(repository).dispatchGuardDepth;
2851
4764
  const eventId = event.id?.value;
@@ -2860,6 +4773,21 @@ const DispatchGuards = {
2860
4773
  guard.active += 1;
2861
4774
  return DispatchGuards.scheduleDispatch(repository, runtime, event, entityId, eventId, journalEventId, depth, guard, guards, dispatch);
2862
4775
  },
4776
+ /**
4777
+ * Queues a guarded dispatch after earlier work for the same Entity.
4778
+ *
4779
+ * @param repository Repository receiving the Event.
4780
+ * @param runtime Storage and tenant context for the dispatch.
4781
+ * @param event Event being delivered.
4782
+ * @param entityId Target Entity identifier.
4783
+ * @param eventId Original Event identifier used by the in-memory check.
4784
+ * @param journalEventId Target-specific identifier used by stored diagnostic Events.
4785
+ * @param depth Maximum completed identifiers and inactive Entity queues to retain.
4786
+ * @param guard Queue and recent completion records for this Entity.
4787
+ * @param guards Repository's collection of Entity queues.
4788
+ * @param dispatch Operation to run unless a completed delivery is found.
4789
+ * @returns Completion of this queued operation, retaining its failure for the caller.
4790
+ */
2863
4791
  scheduleDispatch(repository, runtime, event, entityId, eventId, journalEventId, depth, guard, guards, dispatch) {
2864
4792
  const next = guard.chain.then(() => DispatchGuards.dispatchOnce(repository, runtime, event, entityId, eventId, journalEventId, depth, guard, dispatch));
2865
4793
  // spine-log-boundary: server.repository_dispatch_guard
@@ -2871,6 +4799,13 @@ const DispatchGuards = {
2871
4799
  });
2872
4800
  return next;
2873
4801
  },
4802
+ /**
4803
+ * Gets or creates the serialized dispatch queue for one Entity key.
4804
+ *
4805
+ * @param repository Repository whose queues are being accessed.
4806
+ * @param key Canonical target Entity key.
4807
+ * @returns The Entity queue and the repository collection containing it.
4808
+ */
2874
4809
  guardLane(repository, key) {
2875
4810
  let guards = repositoryDispatchGuards.get(repository);
2876
4811
  if (guards === undefined) {
@@ -2884,6 +4819,20 @@ const DispatchGuards = {
2884
4819
  }
2885
4820
  return { guard, guards };
2886
4821
  },
4822
+ /**
4823
+ * Dispatches an Event unless memory or retained diagnostic Events show completion.
4824
+ *
4825
+ * @param repository Repository receiving the Event.
4826
+ * @param runtime Storage and tenant context for the dispatch.
4827
+ * @param event Event being delivered.
4828
+ * @param entityId Target Entity identifier.
4829
+ * @param eventId Original identifier used for recent in-memory completions.
4830
+ * @param journalEventId Target-specific identifier to find in storage.
4831
+ * @param depth Maximum retained Events to inspect and completions to remember.
4832
+ * @param guard Recent completion records for this Entity.
4833
+ * @param dispatch Operation to run when neither check finds a completed delivery.
4834
+ * @returns Completion of the duplicate check and any required dispatch.
4835
+ */
2887
4836
  async dispatchOnce(repository, runtime, event, entityId, eventId, journalEventId, depth, guard, dispatch) {
2888
4837
  if (guard.completed.has(eventId))
2889
4838
  return;
@@ -2902,6 +4851,13 @@ const DispatchGuards = {
2902
4851
  await dispatch();
2903
4852
  DispatchGuards.rememberGuardCompletion(guard, eventId, depth);
2904
4853
  },
4854
+ /**
4855
+ * Records a successful delivery and discards the oldest excess identifiers.
4856
+ *
4857
+ * @param guard Recent completion records for this Entity.
4858
+ * @param eventId Identifier of the completed Event.
4859
+ * @param depth Maximum number of identifiers to retain.
4860
+ */
2905
4861
  rememberGuardCompletion(guard, eventId, depth) {
2906
4862
  if (!guard.completed.has(eventId)) {
2907
4863
  guard.completed.add(eventId);
@@ -2913,12 +4869,24 @@ const DispatchGuards = {
2913
4869
  guard.completed.delete(expired);
2914
4870
  }
2915
4871
  },
4872
+ /**
4873
+ * Updates the queue list to place this Entity at its most recently used end.
4874
+ *
4875
+ * @param guards Repository collection of Entity queues.
4876
+ * @param key Canonical key of the queue being used.
4877
+ */
2916
4878
  touchGuardLane(guards, key) {
2917
4879
  const index = guards.order.indexOf(key);
2918
4880
  if (index >= 0)
2919
4881
  guards.order.splice(index, 1);
2920
4882
  guards.order.push(key);
2921
4883
  },
4884
+ /**
4885
+ * Removes least-recently-used inactive queues beyond the configured limit.
4886
+ *
4887
+ * @param guards Repository collection of Entity queues.
4888
+ * @param depth Maximum queue count when enough queues are inactive.
4889
+ */
2922
4890
  trimGuardLanes(guards, depth) {
2923
4891
  while (guards.order.length > depth) {
2924
4892
  const index = guards.order.findIndex((key) => guards.lanes.get(key)?.active === 0);
@@ -2930,6 +4898,14 @@ const DispatchGuards = {
2930
4898
  guards.lanes.delete(key);
2931
4899
  }
2932
4900
  },
4901
+ /**
4902
+ * Copies an Event with a target-specific identifier for duplicate checking.
4903
+ *
4904
+ * @param repository Repository supplying the target identifier descriptor.
4905
+ * @param event Source Event, returned unchanged if it has no identifier.
4906
+ * @param entityId Target Entity identifier.
4907
+ * @returns An Event suitable for this target's diagnostic history.
4908
+ */
2933
4909
  guardedJournalEvent(repository, event, entityId) {
2934
4910
  const sourceId = event.id?.value;
2935
4911
  if (sourceId === undefined)
@@ -2941,18 +4917,39 @@ const DispatchGuards = {
2941
4917
  }),
2942
4918
  });
2943
4919
  },
4920
+ /**
4921
+ * Creates an identifier from the original Event identifier and encoded target key.
4922
+ *
4923
+ * @param sourceId Original Event identifier.
4924
+ * @param entityKey Canonical target Entity key.
4925
+ * @returns The target-specific diagnostic Event identifier.
4926
+ */
2944
4927
  guardedJournalEventId(sourceId, entityKey) {
2945
4928
  return `${sourceId}.guard.${encodeURIComponent(entityKey)}`;
2946
4929
  },
4930
+ /**
4931
+ * Encodes an Entity identifier using its repository descriptor and Inbox key format.
4932
+ *
4933
+ * @param repository Repository supplying the identifier descriptor.
4934
+ * @param id Entity identifier to encode.
4935
+ * @returns A stable key for dispatch queues and diagnostic Event identifiers.
4936
+ */
2947
4937
  canonicalEntityIdKey(repository, id) {
2948
4938
  return InboxTargets.key(InboxMessages.inboxTargetId(id, repository.idField));
2949
4939
  },
2950
4940
  };
2951
4941
  Object.freeze(DispatchGuards);
2952
4942
  /**
2953
- * Internal inbox messages operations.
4943
+ * Packs target identifiers and reads typed signals from stored Inbox messages.
2954
4944
  */
2955
4945
  const InboxMessages = {
4946
+ /**
4947
+ * Packs an Entity identifier into the Inbox target representation.
4948
+ *
4949
+ * @param entityId Message, string, or integer Entity identifier.
4950
+ * @param idField Descriptor identifying the Entity's ID field.
4951
+ * @returns The packed target identifier.
4952
+ */
2956
4953
  inboxTargetId(entityId, idField) {
2957
4954
  if (idField.descriptor.fieldKind === "message") {
2958
4955
  return Identifiers.pack(idField.descriptor.message, entityId);
@@ -2965,6 +4962,13 @@ const InboxMessages = {
2965
4962
  return Identifiers.pack("int64", entityId);
2966
4963
  throw new Error("Repository Entity Inbox handoff requires a supported target ID.");
2967
4964
  },
4965
+ /**
4966
+ * Unpacks and validates a stored Inbox target against the repository's ID field.
4967
+ *
4968
+ * @param targetId Packed target identifier stored with the Inbox message.
4969
+ * @param idField Descriptor identifying the expected ID type.
4970
+ * @returns The identifier used to restore the target Entity.
4971
+ */
2968
4972
  targetEntityId(targetId, idField) {
2969
4973
  const descriptor = idField.descriptor;
2970
4974
  let entityId;
@@ -2993,6 +4997,12 @@ const InboxMessages = {
2993
4997
  }
2994
4998
  return RepositoryRoutes.readRouteId(entityId, idField, "command");
2995
4999
  },
5000
+ /**
5001
+ * Reads a Command from an Inbox message labeled for command handling.
5002
+ *
5003
+ * @param message Stored Inbox message to validate and unpack.
5004
+ * @returns The stored Command envelope.
5005
+ */
2996
5006
  readInboxCommand(message) {
2997
5007
  if (message.label !== "HANDLE_COMMAND") {
2998
5008
  throw new Error(`Entity Inbox replay does not handle "${message.label}" messages.`);
@@ -3003,12 +5013,33 @@ const InboxMessages = {
3003
5013
  }
3004
5014
  return command;
3005
5015
  },
5016
+ /**
5017
+ * Reads an Event labeled for a Process Manager reaction.
5018
+ *
5019
+ * @param message Stored Inbox message to validate and unpack.
5020
+ * @returns The stored Event envelope.
5021
+ */
3006
5022
  readPmInboxEvent(message) {
3007
5023
  return InboxMessages.readStoredEvent(message, "REACT_UPON_EVENT", "Entity Inbox replay", "Entity Inbox replay requires a readable stored event.");
3008
5024
  },
5025
+ /**
5026
+ * Reads an Event labeled for a Projection update.
5027
+ *
5028
+ * @param message Stored Inbox message to validate and unpack.
5029
+ * @returns The stored Event envelope.
5030
+ */
3009
5031
  readProjectionInboxEvent(message) {
3010
5032
  return InboxMessages.readStoredEvent(message, "UPDATE_SUBSCRIBER", "Projection inbox replay", "Projection inbox replay requires a readable stored event.");
3011
5033
  },
5034
+ /**
5035
+ * Validates an Inbox operation label and decodes its stored Event bytes.
5036
+ *
5037
+ * @param message Stored Inbox message to inspect.
5038
+ * @param expectedLabel Operation label required by the caller.
5039
+ * @param replayName Human-readable operation name used in label errors.
5040
+ * @param unreadableMessage Error text used when Event decoding fails.
5041
+ * @returns The stored Event envelope.
5042
+ */
3012
5043
  readStoredEvent(message, expectedLabel, replayName, unreadableMessage) {
3013
5044
  if (message.label !== expectedLabel) {
3014
5045
  throw new Error(`${replayName} does not handle "${message.label}" messages.`);
@@ -3028,9 +5059,18 @@ const InboxMessages = {
3028
5059
  };
3029
5060
  Object.freeze(InboxMessages);
3030
5061
  /**
3031
- * Internal inbox replay operations.
5062
+ * Validates and dispatches stored Inbox signals to their recorded Entity targets.
3032
5063
  */
3033
5064
  const InboxReplay = {
5065
+ /**
5066
+ * Dispatches a stored Aggregate Command after checking its tenant and payload.
5067
+ *
5068
+ * @param repository Aggregate repository receiving the Command.
5069
+ * @param routing Registered handlers and message schemas.
5070
+ * @param message Stored Inbox message with the recorded target.
5071
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5072
+ * @returns Any follow-up work that must wait for Inbox completion.
5073
+ */
3034
5074
  async replayAggregateCommand(repository, routing, message, deliveryTenantId) {
3035
5075
  const runtime = repositoryRuntimes.get(repository);
3036
5076
  if (runtime === undefined) {
@@ -3045,6 +5085,15 @@ const InboxReplay = {
3045
5085
  const route = InboxReplay.replayCommandRoute(repository, routing, message, command);
3046
5086
  return await new AggregateCommandExecution(repository, routing, runtime, command).run(route);
3047
5087
  },
5088
+ /**
5089
+ * Routes Process Manager Command or Event replay by its stored operation label.
5090
+ *
5091
+ * @param repository Process Manager repository receiving the signal.
5092
+ * @param routing Registered handlers and message schemas.
5093
+ * @param message Stored Inbox message with the recorded target.
5094
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5095
+ * @returns Any Command follow-up work that must wait for Inbox completion.
5096
+ */
3048
5097
  async replayPmInbox(repository, routing, message, deliveryTenantId) {
3049
5098
  if (message.label === "HANDLE_COMMAND") {
3050
5099
  return await InboxReplay.replayProcessManagerCommand(repository, routing, message, deliveryTenantId);
@@ -3055,6 +5104,15 @@ const InboxReplay = {
3055
5104
  }
3056
5105
  throw new Error(`Entity Inbox replay does not handle "${message.label}" messages.`);
3057
5106
  },
5107
+ /**
5108
+ * Dispatches a stored Process Manager Command after tenant and payload validation.
5109
+ *
5110
+ * @param repository Process Manager repository receiving the Command.
5111
+ * @param routing Registered handlers and message schemas.
5112
+ * @param message Stored Inbox message with the recorded target.
5113
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5114
+ * @returns Any follow-up work that must wait for Inbox completion.
5115
+ */
3058
5116
  async replayProcessManagerCommand(repository, routing, message, deliveryTenantId) {
3059
5117
  const runtime = repositoryRuntimes.get(repository);
3060
5118
  if (runtime === undefined) {
@@ -3066,6 +5124,15 @@ const InboxReplay = {
3066
5124
  const route = InboxReplay.replayCommandRoute(repository, routing, message, command);
3067
5125
  return await new ProcessManagerCommandExecution(repository, routing, runtime, command).run(route);
3068
5126
  },
5127
+ /**
5128
+ * Dispatches a validated Event to the Process Manager target recorded in its Inbox.
5129
+ *
5130
+ * @param repository Process Manager repository receiving the Event.
5131
+ * @param routing Registered handlers and message schemas.
5132
+ * @param message Stored Inbox message with the recorded target.
5133
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5134
+ * @returns Completion of dispatch to the recorded target.
5135
+ */
3069
5136
  async replayProcessManagerEvent(repository, routing, message, deliveryTenantId) {
3070
5137
  const runtime = repositoryRuntimes.get(repository);
3071
5138
  if (runtime === undefined) {
@@ -3078,6 +5145,15 @@ const InboxReplay = {
3078
5145
  const [entityId] = route.entityIds;
3079
5146
  await new ProcessManagerEventExecution(repository, routing, runtime, event).runTarget(entityId, route);
3080
5147
  },
5148
+ /**
5149
+ * Dispatches a validated Event to the Projection target recorded in its Inbox.
5150
+ *
5151
+ * @param repository Projection repository receiving the Event.
5152
+ * @param routing Registered subscribers and message schemas.
5153
+ * @param message Stored Inbox message with the recorded target.
5154
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5155
+ * @returns Completion of dispatch to the recorded target.
5156
+ */
3081
5157
  async replayProjectionEvent(repository, routing, message, deliveryTenantId) {
3082
5158
  const runtime = repositoryRuntimes.get(repository);
3083
5159
  if (runtime === undefined) {
@@ -3090,6 +5166,15 @@ const InboxReplay = {
3090
5166
  const [entityId] = route.entityIds;
3091
5167
  await new ProjectionEventExecution(repository, routing, runtime, event).runTarget(entityId, route);
3092
5168
  },
5169
+ /**
5170
+ * Routes a Projection Inbox message to domain Event or Entity-state update replay.
5171
+ *
5172
+ * @param repository Projection repository receiving the update.
5173
+ * @param routing Registered subscribers and message schemas.
5174
+ * @param message Stored Projection Inbox message.
5175
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5176
+ * @returns Completion of the selected replay operation.
5177
+ */
3093
5178
  async replayProjectionMessage(repository, routing, message, deliveryTenantId) {
3094
5179
  const event = InboxMessages.readProjectionInboxEvent(message);
3095
5180
  if (event.message?.typeUrl === TypeUrls.derive(EntityLog.EntityStateChangedSchema)) {
@@ -3098,6 +5183,16 @@ const InboxReplay = {
3098
5183
  }
3099
5184
  await InboxReplay.replayProjectionEvent(repository, routing, message, deliveryTenantId);
3100
5185
  },
5186
+ /**
5187
+ * Dispatches an Entity-state update to the Projection target recorded in its Inbox.
5188
+ *
5189
+ * @param repository Projection repository receiving the state update.
5190
+ * @param routing Registered state subscribers and schemas.
5191
+ * @param message Stored Inbox message with the recorded target.
5192
+ * @param event Event envelope carrying the Entity-state update.
5193
+ * @param deliveryTenantId Tenant selected by delivery in a multitenant context.
5194
+ * @returns Completion of state-subscriber dispatch to the recorded target.
5195
+ */
3101
5196
  async replayProjectionStateUpdate(repository, routing, message, event, deliveryTenantId) {
3102
5197
  const runtime = repositoryRuntimes.get(repository);
3103
5198
  if (runtime === undefined)
@@ -3108,6 +5203,12 @@ const InboxReplay = {
3108
5203
  const subscribers = route.subscribers;
3109
5204
  await ProjectionEventExecution.runStateTarget(repository, routing, runtime, event, entityId, route, subscribers);
3110
5205
  },
5206
+ /**
5207
+ * Validates a stored Command against its schema and implicit identifier requirements.
5208
+ *
5209
+ * @param routing Registered Command schemas.
5210
+ * @param command Envelope whose payload must remain valid on replay.
5211
+ */
3111
5212
  validateReplayedCommandPayload(routing, command) {
3112
5213
  const commandMessage = EntityInvocation.requireSignalMessage(command.message, "command");
3113
5214
  const commandSchema = RepositoryRoutes.schemaForTypeUrl(routing.commandSchemas, commandMessage.typeUrl, "command");
@@ -3129,6 +5230,13 @@ const InboxReplay = {
3129
5230
  throw new CommandValidationError(implicitId.error);
3130
5231
  }
3131
5232
  },
5233
+ /**
5234
+ * Unpacks and validates a stored Event against its registered schema.
5235
+ *
5236
+ * @param routing Registered Event schemas.
5237
+ * @param event Envelope whose payload must remain valid on replay.
5238
+ * @param invalidPayloadMessage Error text used when the payload cannot be unpacked.
5239
+ */
3132
5240
  validateReplayedEventPayload(routing, event, invalidPayloadMessage = "Projection inbox replay requires a readable event payload.") {
3133
5241
  const eventMessage = EntityInvocation.requireSignalMessage(event.message, "event");
3134
5242
  const eventSchema = RepositoryRoutes.schemaForTypeUrl(routing.eventSchemas, eventMessage.typeUrl, "event");
@@ -3138,6 +5246,13 @@ const InboxReplay = {
3138
5246
  }
3139
5247
  Validate.check(eventSchema, payload);
3140
5248
  },
5249
+ /**
5250
+ * Checks that a replayed Command's tenant matches its delivery tenant.
5251
+ *
5252
+ * @param context Context name and tenant mode; single-tenant contexts need no check.
5253
+ * @param deliveryTenantId Tenant selected by Inbox delivery.
5254
+ * @param command Stored Command containing the original actor context.
5255
+ */
3141
5256
  validateReplayTenant(context, deliveryTenantId, command) {
3142
5257
  if (!context.multitenant) {
3143
5258
  return;
@@ -3153,6 +5268,13 @@ const InboxReplay = {
3153
5268
  throw new Error("Entity Inbox replay stored command tenant does not match.");
3154
5269
  }
3155
5270
  },
5271
+ /**
5272
+ * Checks that a replayed Projection Event's tenant matches its delivery tenant.
5273
+ *
5274
+ * @param context Context name and tenant mode; single-tenant contexts need no check.
5275
+ * @param deliveryTenantId Tenant selected by Inbox delivery.
5276
+ * @param event Stored Event containing the original tenant metadata.
5277
+ */
3156
5278
  validateProjectionReplayTenant(context, deliveryTenantId, event) {
3157
5279
  if (!context.multitenant) {
3158
5280
  return;
@@ -3168,6 +5290,13 @@ const InboxReplay = {
3168
5290
  throw new Error("Projection inbox replay stored event tenant does not match.");
3169
5291
  }
3170
5292
  },
5293
+ /**
5294
+ * Checks that a replayed Process Manager Event's tenant matches its delivery tenant.
5295
+ *
5296
+ * @param context Context name and tenant mode; single-tenant contexts need no check.
5297
+ * @param deliveryTenantId Tenant selected by Inbox delivery.
5298
+ * @param event Stored Event containing the original tenant metadata.
5299
+ */
3171
5300
  validatePmReplayTenant(context, deliveryTenantId, event) {
3172
5301
  if (!context.multitenant) {
3173
5302
  return;
@@ -3183,6 +5312,15 @@ const InboxReplay = {
3183
5312
  throw new Error("Entity Inbox replay stored event tenant does not match.");
3184
5313
  }
3185
5314
  },
5315
+ /**
5316
+ * Restores a Command route from the stored target instead of running routing again.
5317
+ *
5318
+ * @param repository Repository whose Entity type must match the stored target.
5319
+ * @param routing Registered Command schemas.
5320
+ * @param message Inbox message recording the target type and identifier.
5321
+ * @param command Stored Command used to identify the registered handler schema.
5322
+ * @returns A deferred route to the recorded Entity identifier.
5323
+ */
3186
5324
  replayCommandRoute(repository, routing, message, command) {
3187
5325
  const expectedTargetTypeUrl = TypeUrls.derive(repository.stateSchema);
3188
5326
  if (message.inboxId.targetTypeUrl !== expectedTargetTypeUrl) {
@@ -3196,6 +5334,16 @@ const InboxReplay = {
3196
5334
  invocation: "deferred",
3197
5335
  });
3198
5336
  },
5337
+ /**
5338
+ * Restores a single-target Event route from a stored Inbox message.
5339
+ *
5340
+ * @param repository Repository whose Entity type must match the stored target.
5341
+ * @param routing Registered Event schemas.
5342
+ * @param message Inbox message recording the target type and identifier.
5343
+ * @param event Stored Event used to identify the registered handler schema.
5344
+ * @param replayName Operation name used when reporting a target-type mismatch.
5345
+ * @returns A deferred route containing exactly the recorded target identifier.
5346
+ */
3199
5347
  replayEventRoute(repository, routing, message, event, replayName) {
3200
5348
  const expectedTargetTypeUrl = TypeUrls.derive(repository.stateSchema);
3201
5349
  if (message.inboxId.targetTypeUrl !== expectedTargetTypeUrl) {
@@ -3211,6 +5359,15 @@ const InboxReplay = {
3211
5359
  invocation: "deferred",
3212
5360
  });
3213
5361
  },
5362
+ /**
5363
+ * Restores a Projection state-update route and its origin-compatible subscribers.
5364
+ *
5365
+ * @param repository Repository whose Entity type must match the stored target.
5366
+ * @param routing Registered state schemas and subscribers.
5367
+ * @param message Inbox message recording the Projection target.
5368
+ * @param event Stored Entity-state update envelope.
5369
+ * @returns A deferred route with the unpacked state and matching subscribers.
5370
+ */
3214
5371
  replayStateUpdateRoute(repository, routing, message, event) {
3215
5372
  const expectedTargetTypeUrl = TypeUrls.derive(repository.stateSchema);
3216
5373
  if (message.inboxId.targetTypeUrl !== expectedTargetTypeUrl) {
@@ -3228,19 +5385,36 @@ const InboxReplay = {
3228
5385
  entityIds,
3229
5386
  messageFullTypeName: schema.typeName,
3230
5387
  state,
3231
- subscribers: Object.freeze((() => {
3232
- const candidates = routing.stateSubscriptions.get(schema.typeName) ?? [];
3233
- return candidates.filter((subscriber) => (subscriber.handler.origin === "external") === (event.context?.external === true));
3234
- })()),
5388
+ subscribers: InboxReplay.stateSubscribers(routing, schema, event),
3235
5389
  invocation: "deferred",
3236
5390
  });
3237
5391
  },
5392
+ /**
5393
+ * Finds stored state subscribers matching the Event's origin.
5394
+ *
5395
+ * @param routing Registered state subscriptions.
5396
+ * @param schema Decoded state schema.
5397
+ * @param event Stored state-update Event.
5398
+ * @returns Frozen origin-compatible subscriber list.
5399
+ */
5400
+ stateSubscribers(routing, schema, event) {
5401
+ const candidates = routing.stateSubscriptions.get(schema.typeName) ?? [];
5402
+ return Object.freeze(candidates.filter((subscriber) => (subscriber.handler.origin === "external") === (event.context?.external === true)));
5403
+ },
3238
5404
  };
3239
5405
  Object.freeze(InboxReplay);
3240
5406
  /**
3241
- * Internal inbox handoff operations.
5407
+ * Records routed signals in the appropriate Entity or Projection Inbox.
3242
5408
  */
3243
5409
  const InboxHandoff = {
5410
+ /**
5411
+ * Routes a Command and submits it to the target Entity's Inbox.
5412
+ *
5413
+ * @param repository Repository supplying the Command route and Entity type.
5414
+ * @param runtime Inbox, storage, tenant mode, and delivery strategy.
5415
+ * @param command Command envelope to retain and deliver.
5416
+ * @returns Completion of Inbox receipt and its selected delivery operation.
5417
+ */
3244
5418
  async handoffEntityCommand(repository, runtime, command) {
3245
5419
  const route = repository.routeCommand(command);
3246
5420
  const commandId = RepositorySignals.requireCommandId(command);
@@ -3264,6 +5438,15 @@ const InboxHandoff = {
3264
5438
  keepUntil,
3265
5439
  }, deliveryTenantId);
3266
5440
  },
5441
+ /**
5442
+ * Delivers an Event to one Projection target with the duplicate-retention deadline.
5443
+ *
5444
+ * @param repository Repository supplying the Projection type and ID descriptor.
5445
+ * @param runtime Projection Inbox, storage, and tenant mode.
5446
+ * @param event Event envelope to retain and deliver.
5447
+ * @param entityId Routed Projection identifier.
5448
+ * @returns Completion of Inbox receipt and delivery.
5449
+ */
3267
5450
  async handoffProjectionEvent(repository, runtime, event, entityId) {
3268
5451
  const eventId = RepositorySignals.requireEventId(event);
3269
5452
  const whenReceived = new Date();
@@ -3286,6 +5469,15 @@ const InboxHandoff = {
3286
5469
  keepUntil,
3287
5470
  }, deliveryTenantId);
3288
5471
  },
5472
+ /**
5473
+ * Delivers an Event to one Process Manager target using its Entity Inbox.
5474
+ *
5475
+ * @param repository Repository supplying the Process Manager type and ID descriptor.
5476
+ * @param runtime Entity Inbox, storage, tenant mode, and delivery strategy.
5477
+ * @param event Event envelope to retain and deliver.
5478
+ * @param entityId Routed Process Manager identifier.
5479
+ * @returns Completion of Inbox receipt and its selected delivery operation.
5480
+ */
3289
5481
  async handoffPmEvent(repository, runtime, event, entityId) {
3290
5482
  const eventId = RepositorySignals.requireEventId(event);
3291
5483
  const whenReceived = new Date();
@@ -3298,6 +5490,15 @@ const InboxHandoff = {
3298
5490
  });
3299
5491
  await runtime.entityInbox.receive(delivery, InboxHandoff.pmEventInboxInput(repository, eventId.value, event, entityId, keepUntil), deliveryTenantId);
3300
5492
  },
5493
+ /**
5494
+ * Delivers an Event to all routed Process Manager targets through batch receipt.
5495
+ *
5496
+ * @param repository Repository supplying the Process Manager type and ID descriptor.
5497
+ * @param runtime Entity Inbox, storage, tenant mode, and delivery strategy.
5498
+ * @param event Event envelope shared by the target deliveries.
5499
+ * @param entityIds Routed Process Manager identifiers.
5500
+ * @returns Completion of batch receipt and its selected delivery operation.
5501
+ */
3301
5502
  async handoffPmEvents(repository, runtime, event, entityIds) {
3302
5503
  const eventId = RepositorySignals.requireEventId(event);
3303
5504
  const whenReceived = new Date();
@@ -3311,6 +5512,16 @@ const InboxHandoff = {
3311
5512
  const inputs = entityIds.map((entityId) => InboxHandoff.pmEventInboxInput(repository, eventId.value, event, entityId, keepUntil));
3312
5513
  await runtime.entityInbox.receiveAll(delivery, inputs, deliveryTenantId);
3313
5514
  },
5515
+ /**
5516
+ * Builds a pending Process Manager Event delivery for a single target.
5517
+ *
5518
+ * @param repository Repository supplying the target type and ID descriptor.
5519
+ * @param signalId Identifier of the original Event.
5520
+ * @param event Event envelope to pack without repeating validation.
5521
+ * @param entityId Routed Process Manager identifier.
5522
+ * @param keepUntil Duplicate-retention deadline for this receipt.
5523
+ * @returns Input for Entity Inbox receipt.
5524
+ */
3314
5525
  pmEventInboxInput(repository, signalId, event, entityId, keepUntil) {
3315
5526
  return {
3316
5527
  inboxId: {
@@ -3327,9 +5538,16 @@ const InboxHandoff = {
3327
5538
  };
3328
5539
  Object.freeze(InboxHandoff);
3329
5540
  /**
3330
- * Internal repository dispatch operations.
5541
+ * Connects bus dispatchers and Inbox replay to repository execution.
3331
5542
  */
3332
5543
  const RepositoryDispatch = {
5544
+ /**
5545
+ * Creates the bus dispatchers supported by the repository's registered handlers.
5546
+ *
5547
+ * @param repository Repository providing Command and Event routes.
5548
+ * @param routing Registered handlers, schemas, and state-update routes.
5549
+ * @returns Command, Event, and system-Event dispatchers where applicable.
5550
+ */
3333
5551
  createRepositoryDispatchers(repository, routing) {
3334
5552
  const acceptedEventRoutes = new WeakMap();
3335
5553
  const acceptedStateRoutes = new WeakMap();
@@ -3339,6 +5557,13 @@ const RepositoryDispatch = {
3339
5557
  systemEvent: RepositoryDispatch.stateDispatcher(repository, routing, acceptedStateRoutes),
3340
5558
  });
3341
5559
  },
5560
+ /**
5561
+ * Creates a Command dispatcher when the repository accepts Commands.
5562
+ *
5563
+ * @param repository Repository providing Command routes.
5564
+ * @param routing Registered Command handlers and schemas.
5565
+ * @returns The dispatcher, or undefined when no Command schema is registered.
5566
+ */
3342
5567
  commandDispatcher(repository, routing) {
3343
5568
  return routing.commandSchemas.length === 0
3344
5569
  ? undefined
@@ -3347,6 +5572,14 @@ const RepositoryDispatch = {
3347
5572
  dispatch: (command) => RepositoryDispatch.dispatchRepositoryCommand(repository, routing, command),
3348
5573
  });
3349
5574
  },
5575
+ /**
5576
+ * Creates an Event dispatcher that preserves routes computed during acceptance.
5577
+ *
5578
+ * @param repository Repository providing Event routes.
5579
+ * @param routing Registered domestic and external Event handlers and schemas.
5580
+ * @param accepted Cache of routes between Event acceptance and dispatch.
5581
+ * @returns The dispatcher, or undefined when no Event schema is registered.
5582
+ */
3350
5583
  eventDispatcher(repository, routing, accepted) {
3351
5584
  if (routing.eventSchemas.length === 0)
3352
5585
  return undefined;
@@ -3365,6 +5598,14 @@ const RepositoryDispatch = {
3365
5598
  });
3366
5599
  return EventDispatcherOriginSchemas.define(dispatcher, routing.domesticEventSchemas, routing.externalEventSchemas);
3367
5600
  },
5601
+ /**
5602
+ * Creates a system-Event dispatcher for registered Entity-state subscriptions.
5603
+ *
5604
+ * @param repository Repository receiving state updates.
5605
+ * @param routing State schemas, subscribers, and origin-aware routing.
5606
+ * @param accepted Cached accepted routes; null records an ignored update.
5607
+ * @returns The dispatcher, or undefined when no state schema is registered.
5608
+ */
3368
5609
  stateDispatcher(repository, routing, accepted) {
3369
5610
  if (routing.stateSchemas.length === 0)
3370
5611
  return undefined;
@@ -3381,6 +5622,15 @@ const RepositoryDispatch = {
3381
5622
  });
3382
5623
  return EventDispatcherOriginSchemas.define(dispatcher, hasOrigin("domestic") ? [schema] : [], hasOrigin("external") ? [schema] : []);
3383
5624
  },
5625
+ /**
5626
+ * Dispatches a cached state-update route unless acceptance ignored the update.
5627
+ *
5628
+ * @param repository Repository receiving the state update.
5629
+ * @param routing State-update routes and subscribers.
5630
+ * @param event System Event carrying the state update.
5631
+ * @param accepted Cache populated during acceptance and cleared for this Event.
5632
+ * @returns Completion of dispatch, or immediate completion for an ignored update.
5633
+ */
3384
5634
  dispatchAcceptedState(repository, routing, event, accepted) {
3385
5635
  const route = accepted.get(event);
3386
5636
  accepted.delete(event);
@@ -3388,6 +5638,13 @@ const RepositoryDispatch = {
3388
5638
  ? Promise.resolve()
3389
5639
  : RepositoryDispatch.dispatchRepositoryStateUpdate(repository, routing, event, route);
3390
5640
  },
5641
+ /**
5642
+ * Creates replay access for an Aggregate or Process Manager with signal handlers.
5643
+ *
5644
+ * @param repository Repository providing signal routes and Entity identity.
5645
+ * @param routing Registered handlers and message schemas.
5646
+ * @returns The Inbox target and supported operation labels, when applicable.
5647
+ */
3391
5648
  createEntityInboxTarget(repository, routing) {
3392
5649
  if ((repository.entityFamily !== "aggregate" && repository.entityFamily !== "process-manager") ||
3393
5650
  (routing.commandSchemas.length === 0 && routing.eventSchemas.length === 0)) {
@@ -3406,6 +5663,13 @@ const RepositoryDispatch = {
3406
5663
  : InboxReplay.replayPmInbox(repository, routing, message, deliveryTenantId),
3407
5664
  });
3408
5665
  },
5666
+ /**
5667
+ * Creates replay access for a Projection with Event or state subscribers.
5668
+ *
5669
+ * @param repository Repository providing Projection identity and Event routes.
5670
+ * @param routing Registered Event and state subscribers.
5671
+ * @returns The Projection Inbox target, when applicable.
5672
+ */
3409
5673
  createProjectionInboxTarget(repository, routing) {
3410
5674
  if (repository.entityFamily !== "projection" ||
3411
5675
  (routing.eventSchemas.length === 0 && routing.stateSchemas.length === 0)) {
@@ -3416,6 +5680,13 @@ const RepositoryDispatch = {
3416
5680
  replay: (message, deliveryTenantId) => InboxReplay.replayProjectionMessage(repository, routing, message, deliveryTenantId),
3417
5681
  });
3418
5682
  },
5683
+ /**
5684
+ * Creates direct Projection dispatch for ordinary delivery or rebuilding.
5685
+ *
5686
+ * @param repository Repository providing Projection identity and Event routes.
5687
+ * @param routing Registered Projection subscribers and schemas.
5688
+ * @returns A callback that validates routing only when no runtime is bound.
5689
+ */
3419
5690
  createProjectionDirectDispatch(repository, routing) {
3420
5691
  return (event, rebuild) => {
3421
5692
  const runtime = repositoryRuntimes.get(repository);
@@ -3426,6 +5697,15 @@ const RepositoryDispatch = {
3426
5697
  return new ProjectionEventExecution(repository, routing, runtime, event, rebuild).runDirect();
3427
5698
  };
3428
5699
  },
5700
+ /**
5701
+ * Routes an Event to the execution path for the repository's Entity family.
5702
+ *
5703
+ * @param repository Repository providing Entity identity and Event routes.
5704
+ * @param routing Registered Event handlers and schemas.
5705
+ * @param event Event envelope to dispatch.
5706
+ * @param acceptedRoute Previously accepted route, if available.
5707
+ * @returns Completion of routing or runtime dispatch when a runtime is bound.
5708
+ */
3429
5709
  async dispatchRepositoryEvent(repository, routing, event, acceptedRoute) {
3430
5710
  const runtime = repositoryRuntimes.get(repository);
3431
5711
  if (runtime === undefined) {
@@ -3446,12 +5726,31 @@ const RepositoryDispatch = {
3446
5726
  return;
3447
5727
  }
3448
5728
  },
5729
+ /**
5730
+ * Dispatches an Event to each routed Aggregate with its configured duplicate guard.
5731
+ *
5732
+ * @param repository Aggregate repository receiving the Event.
5733
+ * @param routing Registered Aggregate Event handlers and schemas.
5734
+ * @param runtime Bound storage, publisher, and tenant context.
5735
+ * @param event Event envelope to dispatch.
5736
+ * @param route Accepted route listing target Aggregate identifiers.
5737
+ * @returns Completion of all target dispatches in route order.
5738
+ */
3449
5739
  async dispatchAggregateEvent(repository, routing, runtime, event, route) {
3450
5740
  const execution = new AggregateEventExecution(repository, routing, runtime, event);
3451
5741
  for (const entityId of route.entityIds) {
3452
5742
  await DispatchGuards.guardedEntityEventDispatch(repository, runtime, event, entityId, () => execution.runTarget(entityId, route));
3453
5743
  }
3454
5744
  },
5745
+ /**
5746
+ * Delivers a routed Entity-state update to each target Projection Inbox.
5747
+ *
5748
+ * @param repository Projection repository receiving the update.
5749
+ * @param routing Registered state schemas and subscribers.
5750
+ * @param event System Event carrying the state update.
5751
+ * @param acceptedRoute Previously accepted state-update route, if available.
5752
+ * @returns Completion of routing and any required Inbox receipts.
5753
+ */
3455
5754
  async dispatchRepositoryStateUpdate(repository, routing, event, acceptedRoute) {
3456
5755
  const runtime = repositoryRuntimes.get(repository);
3457
5756
  if (runtime === undefined) {
@@ -3466,6 +5765,14 @@ const RepositoryDispatch = {
3466
5765
  await InboxHandoff.handoffProjectionEvent(repository, runtime, event, entityId);
3467
5766
  }
3468
5767
  },
5768
+ /**
5769
+ * Delivers Commands for Aggregates and Process Managers through their Entity Inboxes.
5770
+ *
5771
+ * @param repository Repository providing Command routes and Entity identity.
5772
+ * @param routing Registered Command handler metadata.
5773
+ * @param command Command envelope to dispatch.
5774
+ * @returns Completion of Inbox receipt, or routing validation without a bound runtime.
5775
+ */
3469
5776
  async dispatchRepositoryCommand(repository, routing, command) {
3470
5777
  const runtime = repositoryRuntimes.get(repository);
3471
5778
  if (runtime === undefined) {
@@ -3492,7 +5799,7 @@ const repositoryHistory = {
3492
5799
  /**
3493
5800
  * Creates a cache that extends a repository history only when a caller requests more entries.
3494
5801
  *
3495
- * @typeParam Entry - The history entry type.
5802
+ * @typeParam Entry The history entry type.
3496
5803
  * @param load Loads entries before an optional continuation version.
3497
5804
  * @param versionOf Reads the optional version carried by an entry.
3498
5805
  * @param options Selects strictly descending-version and complete-group behavior.