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

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 (74) hide show
  1. package/README.md +17 -3
  2. package/REFERENCE.md +66 -12
  3. package/dist/context/bounded-context.d.ts +72 -14
  4. package/dist/context/bounded-context.d.ts.map +1 -1
  5. package/dist/context/bounded-context.js +130 -49
  6. package/dist/context/bounded-context.js.map +1 -1
  7. package/dist/context/effective-tenant.d.ts +9 -0
  8. package/dist/context/effective-tenant.d.ts.map +1 -0
  9. package/dist/context/effective-tenant.js +58 -0
  10. package/dist/context/effective-tenant.js.map +1 -0
  11. package/dist/context/tenant-index.d.ts.map +1 -1
  12. package/dist/context/tenant-index.js +70 -1
  13. package/dist/context/tenant-index.js.map +1 -1
  14. package/dist/delivery/delivery.d.ts.map +1 -1
  15. package/dist/delivery/delivery.js +270 -26
  16. package/dist/delivery/delivery.js.map +1 -1
  17. package/dist/delivery/inbox-storage.d.ts +7 -0
  18. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  19. package/dist/delivery/inbox-storage.js +110 -0
  20. package/dist/delivery/inbox-storage.js.map +1 -1
  21. package/dist/entity/entity-metadata.d.ts +2 -2
  22. package/dist/entity/entity-metadata.js +2 -2
  23. package/dist/entity/entity-metadata.js.map +1 -1
  24. package/dist/entity/entity-storage-descriptor.d.ts +24 -4
  25. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  26. package/dist/entity/entity-storage-descriptor.js +69 -23
  27. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  28. package/dist/entity/entity.d.ts +19 -18
  29. package/dist/entity/entity.d.ts.map +1 -1
  30. package/dist/entity/entity.js +61 -24
  31. package/dist/entity/entity.js.map +1 -1
  32. package/dist/entity/spec-scanner.d.ts +13 -1
  33. package/dist/entity/spec-scanner.d.ts.map +1 -1
  34. package/dist/entity/spec-scanner.js +20 -3
  35. package/dist/entity/spec-scanner.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/repository/command-routing.d.ts +63 -10
  40. package/dist/repository/command-routing.d.ts.map +1 -1
  41. package/dist/repository/command-routing.js +41 -8
  42. package/dist/repository/command-routing.js.map +1 -1
  43. package/dist/repository/event-routing.d.ts +64 -9
  44. package/dist/repository/event-routing.d.ts.map +1 -1
  45. package/dist/repository/event-routing.js +42 -7
  46. package/dist/repository/event-routing.js.map +1 -1
  47. package/dist/repository/repository.d.ts +96 -10
  48. package/dist/repository/repository.d.ts.map +1 -1
  49. package/dist/repository/repository.js +437 -133
  50. package/dist/repository/repository.js.map +1 -1
  51. package/dist/repository/state-update-routing.d.ts +67 -8
  52. package/dist/repository/state-update-routing.d.ts.map +1 -1
  53. package/dist/repository/state-update-routing.js +45 -6
  54. package/dist/repository/state-update-routing.js.map +1 -1
  55. package/dist/server/server.d.ts.map +1 -1
  56. package/dist/server/server.js +407 -109
  57. package/dist/server/server.js.map +1 -1
  58. package/dist/services/registered-targets.d.ts +73 -0
  59. package/dist/services/registered-targets.d.ts.map +1 -0
  60. package/dist/services/registered-targets.js +127 -0
  61. package/dist/services/registered-targets.js.map +1 -0
  62. package/dist/services/spine-services.d.ts +26 -12
  63. package/dist/services/spine-services.d.ts.map +1 -1
  64. package/dist/services/spine-services.js +1044 -962
  65. package/dist/services/spine-services.js.map +1 -1
  66. package/dist/stand/stand.d.ts +73 -1
  67. package/dist/stand/stand.d.ts.map +1 -1
  68. package/dist/stand/stand.js +220 -48
  69. package/dist/stand/stand.js.map +1 -1
  70. package/dist/stand/subscription-observer.d.ts.map +1 -1
  71. package/dist/stand/subscription-observer.js +4 -11
  72. package/dist/stand/subscription-observer.js.map +1 -1
  73. package/dist/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +12 -12
@@ -11,17 +11,20 @@
11
11
  * or implied. See the License for the specific language governing permissions and limitations under
12
12
  * the License.
13
13
  */
14
- import { clone, create, ScalarType, toBinary } from "@bufbuild/protobuf";
14
+ import { clone, create, ScalarType, toBinary, } from "@bufbuild/protobuf";
15
15
  import { AnySchema, Int32ValueSchema, Int64ValueSchema, StringValueSchema, TimestampSchema, } from "@bufbuild/protobuf/wkt";
16
16
  import { ValidationException, RejectionThrowable, Validate, TypeUrls, AnyMessages, Identifiers, StringifierRegistry, } from "@spine-event-engine/core";
17
17
  import { CommandContextSchema, ActorContextSchema, CommandIdSchema, CommandSchema, EventContextSchema, EventIdSchema, EventSchema, MessageIdSchema, TenantIdSchema, RejectionEventContextSchema, VersionSchema, EntityOption_Kind, } from "@spine-event-engine/proto";
18
18
  import * as EntityLog from "@spine-event-engine/proto/generated/spine/system/server/entity_log_events_pb.js";
19
19
  import { EntityTypeNameSchema } from "@spine-event-engine/proto/generated/spine/system/server/entity_type_pb.js";
20
- import {} from "@spine-event-engine/storage";
20
+ import { EntityRecordSchema, } from "@spine-event-engine/proto/generated/spine/server/entity/entity_pb.js";
21
+ import { StorageQueryPolicy, } from "@spine-event-engine/storage";
21
22
  import { TenantBoundary } from "@spine-event-engine/storage/provider";
22
23
  import { EntityCommitStorageFactories } from "@spine-event-engine/storage/provider";
23
24
  import { CommandValidationError } from "../bus/command-errors.js";
24
25
  import { SignalPublisher } from "../runtime/signal-publisher.js";
26
+ import { ServerEnvironment, serverEnvironmentAccess } from "../server/server-environment.js";
27
+ import { emitServerWarning } from "../server/server-log.js";
25
28
  import { CommandRoutingInternals, } from "./command-routing.js";
26
29
  import { EventRoutingInternals } from "./event-routing.js";
27
30
  import { StateUpdateRoutingInternals, } from "./state-update-routing.js";
@@ -34,12 +37,14 @@ import { ShardIndex } from "../delivery/shard-index.js";
34
37
  import { Aggregate, ProcessManager, Projection, entityHistoryAccess, processManagerQueryAccess, transactionalEntityAccess, } from "../entity/entity.js";
35
38
  import { describeEntityMetadata, attachEntitySchema, } from "../entity/entity-metadata.js";
36
39
  import { EntityIds, EntityRecords, entityStorageDescriptor, } from "../entity/entity-storage-descriptor.js";
37
- import { SpecScanner } from "../entity/spec-scanner.js";
40
+ import { repositorySpecScanner } from "../entity/spec-scanner.js";
38
41
  import { CommandRegistrationReadiness, } from "../handler/command-registration-readiness.js";
39
42
  import { EventRegistrationReadiness, } from "../handler/event-registration-readiness.js";
40
43
  import { EventHandlerFilters, } from "../handler/event-handler-filter.js";
41
44
  import { SignalMetadata } from "../runtime/signal-metadata.js";
42
45
  import { QueryReader } from "../services/query-reader.js";
46
+ import { RegisteredTargets } from "../services/registered-targets.js";
47
+ import { EffectiveTenants } from "../context/effective-tenant.js";
43
48
  import { HandlerMetadataRegistry, HandlerMetadataValues, } from "../handler/handler-metadata.js";
44
49
  import { DeclaredRejections } from "../handler/declared-rejections.js";
45
50
  import { standAccess } from "../stand/stand.js";
@@ -93,6 +98,7 @@ export class Repository {
93
98
  */
94
99
  constructor(options) {
95
100
  const { entityType, entityFamily, schema, metadata } = RepositoryIdentity.describeOptions(options);
101
+ RepositoryRoutes.requireRoutingEntities(entityType, options);
96
102
  attachEntitySchema(entityType, schema);
97
103
  this.#entityType = entityType;
98
104
  this.#entityFamily = entityFamily;
@@ -186,7 +192,7 @@ export class Repository {
186
192
  * @returns The calculated command route.
187
193
  */
188
194
  routeCommand(command) {
189
- return this.#routing.routeCommand(command);
195
+ return RepositoryRoutingReads.call(this, RepositoryTenants.readCommandTenant(command), (reads) => this.#routing.routeCommand(command, reads));
190
196
  }
191
197
  /**
192
198
  * Routes an Event without invoking a handler.
@@ -201,7 +207,10 @@ export class Repository {
201
207
  * @returns The calculated event route.
202
208
  */
203
209
  routeEvent(event) {
204
- return this.#routing.routeEvent(event);
210
+ return RepositoryRoutingReads.call(this, RepositoryTenants.readEventTenant(event), (reads) => this.#routing.routeEvent(event, reads)).then((route) => {
211
+ RepositoryRoutes.warnLargeRoute(route.messageFullTypeName, this.stateFullTypeName, route.entityIds.length);
212
+ return route;
213
+ });
205
214
  }
206
215
  }
207
216
  const repositorySnapshots = new WeakMap();
@@ -355,7 +364,11 @@ export const repositoryAccess = Object.freeze({
355
364
  if (routing === undefined) {
356
365
  throw new TypeError("State-update routing requires a Repository instance.");
357
366
  }
358
- return routing.routeStateUpdate(event);
367
+ return RepositoryRoutingReads.call(repository, RepositoryTenants.readEventTenant(event), (reads) => routing.routeStateUpdate(event, reads)).then((route) => {
368
+ if (route !== undefined)
369
+ RepositoryRoutes.warnLargeRoute(route.messageFullTypeName, repository.stateSchema.typeName, route.entityIds.length);
370
+ return route;
371
+ });
359
372
  },
360
373
  /**
361
374
  * Looks up the Aggregate or Process Manager inbox replay target.
@@ -436,6 +449,223 @@ const RepositoryCreation = {
436
449
  throw new TypeError(`Repository onCreate must synchronously return a ${entityType.name} instance.`);
437
450
  },
438
451
  };
452
+ /**
453
+ * Binds repository queries to a single routing invocation's tenant and lifetime.
454
+ * Routing integration closes the returned access after its callback settles.
455
+ *
456
+ * @internal
457
+ */
458
+ export const repositoryReadAccess = Object.freeze({
459
+ /**
460
+ * Creates read-only access for a registered repository with an active runtime.
461
+ *
462
+ * @typeParam EntityType The receiving repository's application Entity class.
463
+ * @param repository Receiving repository.
464
+ * @param tenantId Tenant carried by the incoming signal, if multitenant.
465
+ * @returns Bound reads and a close operation for the invocation boundary.
466
+ */
467
+ bind(repository, tenantId) {
468
+ const runtime = repositoryRuntimes.get(repository);
469
+ if (runtime === undefined)
470
+ throw new Error("Repository reads require an active runtime binding.");
471
+ return new RepositoryReadScope(repository, runtime, tenantId);
472
+ },
473
+ });
474
+ /**
475
+ * Keeps route-local read access alive until an asynchronous callback settles.
476
+ */
477
+ const RepositoryRoutingReads = {
478
+ /**
479
+ * Invokes routing with the signal tenant and closes its read scope afterward.
480
+ *
481
+ * @typeParam Result Completed route result.
482
+ * @param repository Receiving repository.
483
+ * @param tenantId Signal tenant, when present.
484
+ * @param callback Route computation using read-only repository access.
485
+ * @returns Completed route result.
486
+ */
487
+ async call(repository, tenantId, callback) {
488
+ const scope = repositoryRuntimes.has(repository)
489
+ ? repositoryReadAccess.bind(repository, tenantId)
490
+ : undefined;
491
+ const reads = scope ?? this.unbound();
492
+ try {
493
+ return await callback(reads);
494
+ }
495
+ finally {
496
+ scope?.close();
497
+ }
498
+ },
499
+ /**
500
+ * Rejects reads when a route is invoked before repository runtime binding.
501
+ *
502
+ * @returns Read operations that report the missing runtime.
503
+ */
504
+ unbound() {
505
+ const unavailable = () => Promise.reject(new Error("Repository reads require an active runtime binding."));
506
+ return { findIds: unavailable, findStates: unavailable, find: unavailable };
507
+ },
508
+ };
509
+ /**
510
+ * Executes read-only queries within one receiving repository and tenant scope.
511
+ *
512
+ * @typeParam EntityType Registered application Entity class.
513
+ */
514
+ class RepositoryReadScope {
515
+ #repository;
516
+ #runtime;
517
+ #context;
518
+ #active = true;
519
+ /**
520
+ * Captures the runtime and validates the incoming signal's effective tenant.
521
+ *
522
+ * @param repository Receiving repository.
523
+ * @param runtime Active repository runtime.
524
+ * @param tenantId Tenant from the signal, when the context is multitenant.
525
+ */
526
+ constructor(repository, runtime, tenantId) {
527
+ this.#repository = repository;
528
+ this.#runtime = runtime;
529
+ this.#context = RepositoryTenants.storageContextForTenant(runtime.context, tenantId);
530
+ }
531
+ /**
532
+ * Reads the typed IDs of all matching current records.
533
+ *
534
+ * @param query Description for the registered state schema.
535
+ * @returns Matching IDs in query order.
536
+ */
537
+ async findIds(query) {
538
+ return (await this.read(query)).map((entry) => entry.id);
539
+ }
540
+ /**
541
+ * Returns cloned complete states from matching current records.
542
+ *
543
+ * @param query Description for the registered state schema.
544
+ * @returns Independent generated state messages in query order.
545
+ */
546
+ async findStates(query) {
547
+ const schema = this.#repository.stateSchema;
548
+ return (await this.read(query)).map((entry) => clone(schema, EntityRecords.unpack(schema, entry.record).state));
549
+ }
550
+ /**
551
+ * Restores each matching application Entity through the repository hook.
552
+ *
553
+ * @param query Description for the registered state schema.
554
+ * @returns Instances with stored Version and lifecycle flags.
555
+ */
556
+ async find(query) {
557
+ const repository = this.#repository;
558
+ return (await this.read(query)).map((entry) => {
559
+ const stored = EntityRecords.unpack(repository.stateSchema, entry.record);
560
+ return RepositoryCreation.create(repository, {
561
+ id: entry.id,
562
+ schema: repository.stateSchema,
563
+ state: clone(repository.stateSchema, stored.state),
564
+ version: stored.versionMessage,
565
+ lifecycle: { archived: stored.archived, deleted: stored.deleted },
566
+ });
567
+ });
568
+ }
569
+ /**
570
+ * Closes this routing invocation's read access to new operations.
571
+ */
572
+ close() {
573
+ this.#active = false;
574
+ }
575
+ /**
576
+ * Reads current storage with the complete normalized plan.
577
+ *
578
+ * @param query Description for the registered state schema.
579
+ * @returns Matching stored records and typed IDs.
580
+ */
581
+ async read(query) {
582
+ if (!this.#active || repositoryRuntimes.get(this.#repository) !== this.#runtime)
583
+ throw new Error("Repository read access has finished.");
584
+ if (query.schema !== this.#repository.stateSchema)
585
+ throw new TypeError("Repository query must select the receiving Entity state schema.");
586
+ const plan = RepositoryReadPredicates.plan(query.buildPlan());
587
+ const storage = RepositoryStorage.openReadEntityStorage(this.#runtime.storageFactory, RepositoryStorage.entityStorageInput(this.#repository, this.#context));
588
+ try {
589
+ return await storage.current.query(plan);
590
+ }
591
+ finally {
592
+ storage.close();
593
+ }
594
+ }
595
+ }
596
+ /**
597
+ * Applies repository lifecycle defaults only when IDs and lifecycle filters are absent.
598
+ */
599
+ const RepositoryReadPredicates = {
600
+ /**
601
+ * Builds an exhaustive normalized plan and validates it before provider access.
602
+ *
603
+ * @param compiled Storage-neutral query plan from the shared description.
604
+ * @returns Plan with repository lifecycle selection and explicit application bounds.
605
+ */
606
+ plan(compiled) {
607
+ const predicate = this.flattenConjunctions(this.withLifecycleDefault(compiled.predicate));
608
+ const plan = {
609
+ predicate,
610
+ ...(compiled.order === undefined ? {} : { order: compiled.order }),
611
+ ...(compiled.limit === undefined ? {} : { limit: compiled.limit }),
612
+ exhaustive: true,
613
+ };
614
+ StorageQueryPolicy.validate(plan, {
615
+ comparisons: ["equal", "greaterThan", "lessThan", "greaterOrEqual", "lessOrEqual"],
616
+ features: ["either", "nested", "order", "limit"],
617
+ });
618
+ return plan;
619
+ },
620
+ /**
621
+ * Adds active-record filters when no ID or lifecycle clause occurs in the tree.
622
+ *
623
+ * @param predicate Compiled query predicate, when present.
624
+ * @returns Predicate selected for repository reads.
625
+ */
626
+ withLifecycleDefault(predicate) {
627
+ if (predicate !== undefined && this.explicitSelection(predicate))
628
+ return predicate;
629
+ const active = [
630
+ { kind: "comparison", column: "archived", operator: "equal", value: false },
631
+ { kind: "comparison", column: "deleted", operator: "equal", value: false },
632
+ ];
633
+ return {
634
+ kind: "all",
635
+ predicates: predicate === undefined ? active : [predicate, ...active],
636
+ };
637
+ },
638
+ /**
639
+ * Normalizes adjacent conjunctions so providers that require flat filters can admit them.
640
+ *
641
+ * @param predicate Compiled predicate after lifecycle selection.
642
+ * @returns Equivalent predicate with no directly nested conjunctions.
643
+ */
644
+ flattenConjunctions(predicate) {
645
+ if (predicate.kind === "comparison" || predicate.kind === "ids")
646
+ return predicate;
647
+ const children = predicate.predicates.map((child) => this.flattenConjunctions(child));
648
+ return {
649
+ kind: predicate.kind,
650
+ predicates: predicate.kind === "all"
651
+ ? children.flatMap((child) => (child.kind === "all" ? child.predicates : [child]))
652
+ : children,
653
+ };
654
+ },
655
+ /**
656
+ * Checks for an explicit ID or lifecycle clause at any predicate depth.
657
+ *
658
+ * @param predicate Compiled predicate to inspect.
659
+ * @returns Whether the application selected IDs or lifecycle state explicitly.
660
+ */
661
+ explicitSelection(predicate) {
662
+ if (predicate.kind === "ids")
663
+ return true;
664
+ if (predicate.kind === "comparison")
665
+ return predicate.column === "archived" || predicate.column === "deleted";
666
+ return predicate.predicates.some((child) => this.explicitSelection(child));
667
+ },
668
+ };
439
669
  /**
440
670
  * Loads and persists Aggregate state within one tenant storage context.
441
671
  */
@@ -512,11 +742,9 @@ class AggregateExecutionSupport {
512
742
  const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
513
743
  const state = RepositoryEntities.repositoryState(loaded.entity);
514
744
  const versionMessage = RepositoryEntities.repositoryVersion(loaded.entity);
515
- const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(this.#storageContext.tenantId, versionMessage, lifecycle));
745
+ const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(this.#storageContext.tenantId, versionMessage, lifecycle), this.#repository.metadata);
516
746
  try {
517
- const outcome = await this.#commitAggregateRecord(loaded, entityId, state, versionMessage, lifecycle, events, diagnostics);
518
- if (outcome !== "committed")
519
- throw new Error("Concurrent Aggregate state commit conflict.");
747
+ await this.#commitAggregateRecord(loaded, entityId, events, diagnostics, deferred);
520
748
  entityStateHistoryCaches.get(loaded.entity)?.clear();
521
749
  }
522
750
  catch (error) {
@@ -547,20 +775,17 @@ class AggregateExecutionSupport {
547
775
  *
548
776
  * @param loaded Loaded Aggregate and commit storage.
549
777
  * @param entityId Aggregate identifier.
550
- * @param state Accepted Aggregate state.
551
- * @param version Current Spine Version.
552
- * @param lifecycle Accepted lifecycle flags.
553
778
  * @param events Produced Events to retain and publish.
554
779
  * @param diagnostics Events to retain in the diagnostic history.
555
- * @returns The storage commit outcome.
780
+ * @param deferred Stand update containing the prepared record.
781
+ * @returns Completion after the storage commit.
556
782
  */
557
- #commitAggregateRecord(loaded, entityId, state, version, lifecycle, events, diagnostics) {
558
- const record = EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle);
783
+ #commitAggregateRecord(loaded, entityId, events, diagnostics, deferred) {
784
+ const record = RepositoryStand.preparedRecord(this.#repository, deferred, entityId);
559
785
  return loaded.commits.commit({
560
786
  context: this.#storageContext,
561
787
  entity: loaded.storageInput,
562
788
  entityId,
563
- ...(loaded.current === undefined ? {} : { expected: loaded.current }),
564
789
  next: record,
565
790
  ...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
566
791
  ? { states: [record] }
@@ -670,7 +895,7 @@ class AggregateCommandExecution {
670
895
  */
671
896
  async run(replayedRoute) {
672
897
  void RepositorySignals.requireCommandId(this.#command);
673
- const intake = this.#readIntake(replayedRoute);
898
+ const intake = await this.#readIntake(replayedRoute);
674
899
  if (intake === undefined)
675
900
  return undefined;
676
901
  return this.#runAssignee(intake);
@@ -681,11 +906,11 @@ class AggregateCommandExecution {
681
906
  * @param replayedRoute Accepted inbox route, when present.
682
907
  * @returns Decoded message and route with an assignee, or `undefined`.
683
908
  */
684
- #readIntake(replayedRoute) {
909
+ async #readIntake(replayedRoute) {
685
910
  const commandMessage = EntityInvocation.requireSignalMessage(this.#command.message, "command");
686
911
  const commandSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.commandSchemas, commandMessage.typeUrl, "command");
687
912
  const message = EntityInvocation.unpackRequired(commandMessage, commandSchema, "command");
688
- const route = replayedRoute ?? this.#repository.routeCommand(this.#command);
913
+ const route = replayedRoute ?? (await this.#repository.routeCommand(this.#command));
689
914
  const assignee = this.#routing.commandReadiness?.findCommandAssignee(route.messageFullTypeName);
690
915
  return assignee === undefined ? undefined : Object.freeze({ message, route, assignee });
691
916
  }
@@ -1077,7 +1302,7 @@ class ProjectionEventExecution {
1077
1302
  * @returns Completion after inbox handoff.
1078
1303
  */
1079
1304
  async run(acceptedRoute) {
1080
- const intake = this.#readIntake(acceptedRoute);
1305
+ const intake = await this.#readIntake(acceptedRoute);
1081
1306
  if (intake.subscribers.length === 0) {
1082
1307
  return;
1083
1308
  }
@@ -1093,7 +1318,7 @@ class ProjectionEventExecution {
1093
1318
  * @returns Completion after target delivery.
1094
1319
  */
1095
1320
  async runTarget(entityId, acceptedRoute) {
1096
- const intake = this.#readIntake(acceptedRoute);
1321
+ const intake = await this.#readIntake(acceptedRoute);
1097
1322
  if (intake.subscribers.length === 0) {
1098
1323
  return;
1099
1324
  }
@@ -1106,7 +1331,7 @@ class ProjectionEventExecution {
1106
1331
  * @returns Completion after all targets receive the Event.
1107
1332
  */
1108
1333
  async runDirect(acceptedRoute) {
1109
- const intake = this.#readIntake(acceptedRoute);
1334
+ const intake = await this.#readIntake(acceptedRoute);
1110
1335
  if (intake.subscribers.length === 0) {
1111
1336
  return;
1112
1337
  }
@@ -1154,8 +1379,8 @@ class ProjectionEventExecution {
1154
1379
  * @param acceptedRoute Route accepted before delivery, when present.
1155
1380
  * @returns Route and matching subscribers.
1156
1381
  */
1157
- #readIntake(acceptedRoute) {
1158
- const route = acceptedRoute ?? this.#repository.routeEvent(this.#event);
1382
+ async #readIntake(acceptedRoute) {
1383
+ const route = acceptedRoute ?? (await this.#repository.routeEvent(this.#event));
1159
1384
  const packedMessage = EntityInvocation.requireSignalMessage(this.#event.message, "event");
1160
1385
  const eventSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.eventSchemas, packedMessage.typeUrl, "event");
1161
1386
  const message = EntityInvocation.unpackRequired(packedMessage, eventSchema, "event");
@@ -1182,8 +1407,8 @@ class ProjectionEventExecution {
1182
1407
  const state = RepositoryEntities.repositoryState(loaded.entity);
1183
1408
  const version = RepositoryEntities.repositoryVersion(loaded.entity);
1184
1409
  const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
1185
- const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(tenantOptions.tenantId, version, lifecycle));
1186
- await this.#commitProjectionOrCancel(loaded, entityId, state, version, lifecycle, deferred);
1410
+ const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(tenantOptions.tenantId, version, lifecycle), this.#repository.metadata);
1411
+ await this.#commitProjectionOrCancel(loaded, entityId, deferred);
1187
1412
  this.#notifyProjection(deferred);
1188
1413
  this.#publishProjectionChange(loaded, oldState, mode, state, lifecycle, version);
1189
1414
  }
@@ -1192,17 +1417,12 @@ class ProjectionEventExecution {
1192
1417
  *
1193
1418
  * @param loaded Loaded Projection and commit storage.
1194
1419
  * @param entityId Projection identifier.
1195
- * @param state Accepted Projection state.
1196
- * @param version Current Spine Version.
1197
- * @param lifecycle Accepted lifecycle flags.
1198
1420
  * @param deferred Deferred Stand update to cancel if storage rejects the commit.
1199
1421
  * @returns Completion after a successful storage commit.
1200
1422
  */
1201
- async #commitProjectionOrCancel(loaded, entityId, state, version, lifecycle, deferred) {
1423
+ async #commitProjectionOrCancel(loaded, entityId, deferred) {
1202
1424
  try {
1203
- const outcome = await this.#commitProjectionRecord(loaded, entityId, state, version, lifecycle);
1204
- if (outcome !== "committed")
1205
- throw new Error("Concurrent Projection state commit conflict.");
1425
+ await this.#commitProjectionRecord(loaded, entityId, deferred);
1206
1426
  }
1207
1427
  catch (error) {
1208
1428
  deferred.cancel();
@@ -1227,18 +1447,15 @@ class ProjectionEventExecution {
1227
1447
  *
1228
1448
  * @param loaded Loaded Projection and commit storage.
1229
1449
  * @param entityId Projection identifier.
1230
- * @param state Accepted Projection state.
1231
- * @param version Current Spine Version.
1232
- * @param lifecycle Accepted lifecycle flags.
1233
- * @returns The storage commit outcome.
1450
+ * @param deferred Stand update containing the prepared record.
1451
+ * @returns Completion after the storage commit.
1234
1452
  */
1235
- #commitProjectionRecord(loaded, entityId, state, version, lifecycle) {
1236
- const record = EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle);
1453
+ #commitProjectionRecord(loaded, entityId, deferred) {
1454
+ const record = RepositoryStand.preparedRecord(this.#repository, deferred, entityId);
1237
1455
  return loaded.commits.commit({
1238
1456
  context: loaded.storageInput.context,
1239
1457
  entity: loaded.storageInput,
1240
1458
  entityId,
1241
- ...(loaded.current === undefined ? {} : { expected: loaded.current }),
1242
1459
  next: record,
1243
1460
  ...(RepositoryStorage.historyConfiguration(this.#repository).stateHistory
1244
1461
  ? { states: [record] }
@@ -1362,13 +1579,38 @@ const ProcessManagerQueries = Object.freeze({
1362
1579
  const context = actorContext === undefined
1363
1580
  ? create(ActorContextSchema)
1364
1581
  : clone(ActorContextSchema, actorContext);
1365
- if (tenantId !== undefined) {
1366
- context.tenantId = RepositoryTenants.require(tenantId);
1367
- }
1368
- return processManagerQueryAccess.bind(entity, async (plan, schema, query) => {
1369
- const results = await QueryReader.read(runtime.stand, schema, plan, tenantId, 10_000, query);
1370
- return Object.freeze(results.map((result) => clone(schema, result.state)));
1371
- }, context);
1582
+ const effectiveTenant = EffectiveTenants.current(runtime.context.multitenant, tenantId);
1583
+ context.tenantId = effectiveTenant;
1584
+ return processManagerQueryAccess.bind(entity, (plan, schema, query) => ProcessManagerQueries.read(runtime, effectiveTenant, plan, schema, query), context);
1585
+ },
1586
+ /**
1587
+ * Routes a handler read to the registered state target before reaching storage.
1588
+ *
1589
+ * @typeParam Schema State schema supplied by the handler.
1590
+ * @param runtime Current context services and local Stand.
1591
+ * @param tenantId Effective tenant of the current operation.
1592
+ * @param plan Compiled read criteria.
1593
+ * @param schema State schema supplied by the handler.
1594
+ * @param query Wire query carrying the actor and requested fields.
1595
+ * @returns Detached state snapshots decoded with the caller's schema.
1596
+ */
1597
+ async read(runtime, tenantId, plan, schema, query) {
1598
+ const routes = RegisteredTargets.forStand(runtime.stand);
1599
+ const typeUrl = TypeUrls.derive(schema);
1600
+ const target = routes?.find(typeUrl);
1601
+ if (routes !== undefined && target === undefined)
1602
+ throw new Error(`No bounded context registered query target "${typeUrl}".`);
1603
+ if (target !== undefined && target.context.stand() !== runtime.stand) {
1604
+ const visibility = target.repository.metadata.visibility;
1605
+ if (visibility !== "query" && visibility !== "full")
1606
+ throw new Error(`Query target "${typeUrl}" does not have query visibility.`);
1607
+ }
1608
+ const registeredSchema = target?.repository.stateSchema ?? schema;
1609
+ if (target !== undefined)
1610
+ routes?.validate(target, plan);
1611
+ const destination = EffectiveTenants.destination(tenantId, target?.context.isMultitenant ?? runtime.context.multitenant);
1612
+ const results = await QueryReader.read(target?.context.stand() ?? runtime.stand, registeredSchema, plan, destination, 10_000, query);
1613
+ return Object.freeze(results.map((result) => clone(schema, result.state)));
1372
1614
  },
1373
1615
  });
1374
1616
  /**
@@ -1433,8 +1675,8 @@ class ProcessManagerExecutionSupport {
1433
1675
  const entityId = loaded.entity.id;
1434
1676
  const state = RepositoryEntities.repositoryState(loaded.entity);
1435
1677
  const lifecycle = RepositoryEntities.repositoryLifecycle(loaded.entity);
1436
- const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(options.tenantId, version, lifecycle));
1437
- await this.#storeProcessManagerRecord(loaded, entityId, state, version, lifecycle, events, deferred);
1678
+ const deferred = await standAccess.deferUpdate(this.#runtime.stand, this.#repository.stateSchema, state, RepositoryStand.standUpdateOptions(options.tenantId, version, lifecycle), this.#repository.metadata);
1679
+ await this.#storeProcessManagerRecord(loaded, entityId, events, deferred);
1438
1680
  this.#notifyProcessManager(deferred, events);
1439
1681
  return true;
1440
1682
  }
@@ -1456,19 +1698,13 @@ class ProcessManagerExecutionSupport {
1456
1698
  *
1457
1699
  * @param loaded Process Manager and commit storage.
1458
1700
  * @param entityId Process Manager identifier.
1459
- * @param state Accepted Process Manager state.
1460
- * @param version Current Spine Version.
1461
- * @param lifecycle Accepted lifecycle flags.
1462
1701
  * @param events Diagnostic Events to retain when configured.
1463
1702
  * @param deferred Stand update to notify or cancel.
1464
1703
  * @returns Completion after durable persistence.
1465
1704
  */
1466
- async #storeProcessManagerRecord(loaded, entityId, state, version, lifecycle, events, deferred) {
1705
+ async #storeProcessManagerRecord(loaded, entityId, events, deferred) {
1467
1706
  try {
1468
- const outcome = await this.#commitProcessManagerRecord(loaded, entityId, state, version, lifecycle, events);
1469
- if (outcome !== "committed") {
1470
- throw new Error("Concurrent Process Manager state commit conflict.");
1471
- }
1707
+ await this.#commitProcessManagerRecord(loaded, entityId, events, deferred);
1472
1708
  entityStateHistoryCaches.get(loaded.entity)?.clear();
1473
1709
  }
1474
1710
  catch (error) {
@@ -1481,20 +1717,17 @@ class ProcessManagerExecutionSupport {
1481
1717
  *
1482
1718
  * @param loaded Loaded Process Manager and commit storage.
1483
1719
  * @param entityId Process Manager identifier.
1484
- * @param state Accepted Process Manager state.
1485
- * @param version Current Spine Version.
1486
- * @param lifecycle Accepted lifecycle flags.
1487
1720
  * @param events Diagnostic Events to retain when configured.
1488
- * @returns The storage commit outcome.
1721
+ * @param deferred Stand update containing the prepared record.
1722
+ * @returns Completion after the storage commit.
1489
1723
  */
1490
- #commitProcessManagerRecord(loaded, entityId, state, version, lifecycle, events) {
1724
+ #commitProcessManagerRecord(loaded, entityId, events, deferred) {
1491
1725
  const history = RepositoryStorage.historyConfiguration(this.#repository);
1492
- const record = EntityRecords.pack(this.#repository.stateSchema, entityId, state, version, lifecycle);
1726
+ const record = RepositoryStand.preparedRecord(this.#repository, deferred, entityId);
1493
1727
  return loaded.commits.commit({
1494
1728
  context: loaded.storageInput.context,
1495
1729
  entity: loaded.storageInput,
1496
1730
  entityId,
1497
- ...(loaded.current === undefined ? {} : { expected: loaded.current }),
1498
1731
  next: record,
1499
1732
  ...(history.stateHistory ? { states: [record] } : {}),
1500
1733
  ...(history.processManagerEventHistory
@@ -1549,7 +1782,7 @@ class ProcessManagerCommandExecution {
1549
1782
  */
1550
1783
  async run(replayedRoute) {
1551
1784
  RepositorySignals.requireCommandId(this.#command);
1552
- const intake = this.#readIntake(replayedRoute);
1785
+ const intake = await this.#readIntake(replayedRoute);
1553
1786
  if (intake === undefined)
1554
1787
  return undefined;
1555
1788
  const tenantOptions = RepositoryTenants.commandStandOptions(this.#runtime.context, this.#command);
@@ -1580,11 +1813,11 @@ class ProcessManagerCommandExecution {
1580
1813
  * @param replayedRoute Accepted inbox route, when present.
1581
1814
  * @returns Decoded Command, route, and assignee, or `undefined`.
1582
1815
  */
1583
- #readIntake(replayedRoute) {
1816
+ async #readIntake(replayedRoute) {
1584
1817
  const commandMessage = EntityInvocation.requireSignalMessage(this.#command.message, "command");
1585
1818
  const commandSchema = RepositoryRoutes.schemaForTypeUrl(this.#routing.commandSchemas, commandMessage.typeUrl, "command");
1586
1819
  const message = EntityInvocation.unpackRequired(commandMessage, commandSchema, "command");
1587
- const route = replayedRoute ?? this.#repository.routeCommand(this.#command);
1820
+ const route = replayedRoute ?? (await this.#repository.routeCommand(this.#command));
1588
1821
  const assignee = this.#routing.commandReadiness?.findCommandAssignee(route.messageFullTypeName);
1589
1822
  return assignee === undefined ? undefined : Object.freeze({ assignee, message, route });
1590
1823
  }
@@ -2612,7 +2845,7 @@ const RepositorySignals = {
2612
2845
  * @returns New Event context with producer metadata.
2613
2846
  */
2614
2847
  eventContextWithProducer(context, repository, entityId, version) {
2615
- const producerId = EntityIds.pack(repository.stateSchema, entityId);
2848
+ const producerId = EntityIds.packField(repository.idField.descriptor, entityId);
2616
2849
  return create(EventContextSchema, {
2617
2850
  ...context,
2618
2851
  producerId,
@@ -2953,7 +3186,7 @@ class EntityStateChangePublishing {
2953
3186
  * @returns Packed identifier.
2954
3187
  */
2955
3188
  #packEntityId(repository, entityId) {
2956
- return EntityIds.pack(repository.stateSchema, entityId);
3189
+ return EntityIds.packField(repository.idField.descriptor, entityId);
2957
3190
  }
2958
3191
  }
2959
3192
  const EntityStateChangePublisher = Object.freeze(new EntityStateChangePublishing());
@@ -3079,7 +3312,7 @@ class HandlerDispatchPublishing {
3079
3312
  * @returns Packed identifier.
3080
3313
  */
3081
3314
  #packEntityId(repository, entityId) {
3082
- return EntityIds.pack(repository.stateSchema, entityId);
3315
+ return EntityIds.packField(repository.idField.descriptor, entityId);
3083
3316
  }
3084
3317
  /**
3085
3318
  * Packs a subscriber or reactor dispatch diagnostic message.
@@ -3112,8 +3345,23 @@ const HandlerDispatchPublisher = Object.freeze(new HandlerDispatchPublishing());
3112
3345
  Object.freeze(RepositorySignals);
3113
3346
  /**
3114
3347
  * Internal repository stand operations.
3348
+ * @internal
3115
3349
  */
3116
- const RepositoryStand = {
3350
+ export const RepositoryStand = {
3351
+ /**
3352
+ * Returns the validated Stand snapshot with the authoritative routed ID.
3353
+ *
3354
+ * @param repository Supplies the state schema and validated ID descriptor.
3355
+ * @param deferred Contains the prepared Stand record.
3356
+ * @param entityId Authoritative routed Entity identifier.
3357
+ * @returns An isolated prepared record with the authoritative packed ID.
3358
+ */
3359
+ preparedRecord(repository, deferred, entityId) {
3360
+ const expectedId = EntityIds.packField(repository.idField.descriptor, entityId);
3361
+ const record = clone(EntityRecordSchema, deferred.record);
3362
+ record.entityId = expectedId;
3363
+ return record;
3364
+ },
3117
3365
  /**
3118
3366
  * Builds tenant, Version, and lifecycle options for a Stand update.
3119
3367
  *
@@ -3242,16 +3490,16 @@ const RepositoryTenants = {
3242
3490
  * @returns Tenant-aware storage context.
3243
3491
  */
3244
3492
  storageContextForCommand(context, command) {
3245
- if (!context.multitenant) {
3246
- return Object.freeze({ name: context.name, multitenant: false });
3247
- }
3248
3493
  const tenantId = RepositoryTenants.readCommandTenant(command);
3249
- if (tenantId === undefined)
3494
+ if (context.multitenant && tenantId === undefined)
3250
3495
  throw new Error(`Multitenant command for "${context.name}" requires tenantId.`);
3496
+ const effective = EffectiveTenants.current(context.multitenant, tenantId);
3497
+ if (!context.multitenant)
3498
+ return Object.freeze({ name: context.name, multitenant: false });
3251
3499
  return Object.freeze({
3252
3500
  name: context.name,
3253
3501
  multitenant: true,
3254
- tenantId: RepositoryTenants.require(tenantId),
3502
+ tenantId: effective,
3255
3503
  });
3256
3504
  },
3257
3505
  /**
@@ -3262,16 +3510,16 @@ const RepositoryTenants = {
3262
3510
  * @returns Tenant-aware storage context.
3263
3511
  */
3264
3512
  storageContextForEvent(context, event) {
3265
- if (!context.multitenant) {
3266
- return Object.freeze({ name: context.name, multitenant: false });
3267
- }
3268
3513
  const tenantId = RepositoryTenants.readEventTenant(event);
3269
- if (tenantId === undefined)
3514
+ if (context.multitenant && tenantId === undefined)
3270
3515
  throw new Error(`Multitenant event for "${context.name}" requires tenantId.`);
3516
+ const effective = EffectiveTenants.current(context.multitenant, tenantId);
3517
+ if (!context.multitenant)
3518
+ return Object.freeze({ name: context.name, multitenant: false });
3271
3519
  return Object.freeze({
3272
3520
  name: context.name,
3273
3521
  multitenant: true,
3274
- tenantId: RepositoryTenants.require(tenantId),
3522
+ tenantId: effective,
3275
3523
  });
3276
3524
  },
3277
3525
  /**
@@ -3282,14 +3530,15 @@ const RepositoryTenants = {
3282
3530
  * @returns Tenant-aware storage context.
3283
3531
  */
3284
3532
  storageContextForTenant(context, tenantId) {
3533
+ if (context.multitenant && tenantId === undefined)
3534
+ throw new Error(`Multitenant storage for "${context.name}" requires tenantId.`);
3535
+ const effective = EffectiveTenants.current(context.multitenant, tenantId);
3285
3536
  if (!context.multitenant)
3286
3537
  return Object.freeze({ name: context.name, multitenant: false });
3287
- if (tenantId === undefined)
3288
- throw new Error(`Multitenant storage for "${context.name}" requires tenantId.`);
3289
3538
  return Object.freeze({
3290
3539
  name: context.name,
3291
3540
  multitenant: true,
3292
- tenantId: RepositoryTenants.require(tenantId),
3541
+ tenantId: effective,
3293
3542
  });
3294
3543
  },
3295
3544
  /**
@@ -3548,6 +3797,29 @@ Object.freeze(RepositoryHandlers);
3548
3797
  * Internal repository routes operations.
3549
3798
  */
3550
3799
  const RepositoryRoutes = {
3800
+ /**
3801
+ * Checks every class-aware route declaration against its receiving Entity.
3802
+ *
3803
+ * @param registered Entity constructor registered by the repository.
3804
+ * @param routes Route declaration options selected for the repository.
3805
+ */
3806
+ requireRoutingEntities(registered, routes) {
3807
+ RepositoryRoutes.requireRoutingEntity(registered, CommandRoutingInternals.entityType(routes.commandRouting), "Command");
3808
+ RepositoryRoutes.requireRoutingEntity(registered, EventRoutingInternals.entityType(routes.eventRouting), "Event");
3809
+ RepositoryRoutes.requireRoutingEntity(registered, StateUpdateRoutingInternals.entityType(routes.stateUpdateRouting), "State-update");
3810
+ },
3811
+ /**
3812
+ * Rejects class-aware declarations for a different registered Entity constructor.
3813
+ *
3814
+ * @param registered Entity constructor registered by the repository.
3815
+ * @param selected Constructor selected by class-aware declarations, if any.
3816
+ * @param kind Command, Event, or state-update route kind for diagnostics.
3817
+ */
3818
+ requireRoutingEntity(registered, selected, kind) {
3819
+ if (selected !== undefined && selected !== registered) {
3820
+ throw new TypeError(`${kind} routing Entity class "${selected.name}" does not match "${registered.name}".`);
3821
+ }
3822
+ },
3551
3823
  /**
3552
3824
  * Builds immutable routing from an Entity constructor and its declarations.
3553
3825
  *
@@ -3816,7 +4088,7 @@ const RepositoryRoutes = {
3816
4088
  * @returns Function that routes one Command.
3817
4089
  */
3818
4090
  commandSelector(schemas, readiness, routes, idField) {
3819
- return (command) => RepositoryRoutes.routeCommand(command, readiness.command, schemas.command, idField, routes.command);
4091
+ return (command, reads) => RepositoryRoutes.routeCommand(command, readiness.command, schemas.command, idField, routes.command, reads);
3820
4092
  },
3821
4093
  /**
3822
4094
  * Builds an Event route function from readiness and custom declarations.
@@ -3830,7 +4102,7 @@ const RepositoryRoutes = {
3830
4102
  * @returns Function that routes one Event.
3831
4103
  */
3832
4104
  eventSelector(schemas, readiness, filters, routes, idField) {
3833
- return (event) => RepositoryRoutes.routeEvent(event, readiness.event, filters.commandReactionMap, schemas.event, idField, routes.event);
4105
+ return (event, reads) => RepositoryRoutes.routeEvent(event, readiness.event, filters.commandReactionMap, schemas.event, idField, routes.event, reads);
3834
4106
  },
3835
4107
  /**
3836
4108
  * Builds a state-update route function for Projection subscriptions.
@@ -3843,7 +4115,7 @@ const RepositoryRoutes = {
3843
4115
  * @returns Function that routes one state-change Event.
3844
4116
  */
3845
4117
  stateSelector(schemas, subscriptions, routes, idField) {
3846
- return (event) => RepositoryRoutes.routeStateUpdate(event, schemas.state, subscriptions, idField, routes.state);
4118
+ return (event, reads) => RepositoryRoutes.routeStateUpdate(event, schemas.state, subscriptions, idField, routes.state, reads);
3847
4119
  },
3848
4120
  /**
3849
4121
  * Routes a Command to its registered assignee and target Entity ID.
@@ -3854,9 +4126,10 @@ const RepositoryRoutes = {
3854
4126
  * @param schemas Accepted Command schemas.
3855
4127
  * @param targetIdField Canonical target Entity ID field.
3856
4128
  * @param commandRoutes Custom Command routes by schema.
4129
+ * @param reads Query access for this routing invocation.
3857
4130
  * @returns Deferred Command route for one target.
3858
4131
  */
3859
- routeCommand(command, readiness, schemas, targetIdField, commandRoutes) {
4132
+ async routeCommand(command, readiness, schemas, targetIdField, commandRoutes, reads) {
3860
4133
  const message = command.message;
3861
4134
  if (message === undefined || message.typeUrl === "") {
3862
4135
  throw new Error("Repository command routing requires command.message.typeUrl.");
@@ -3866,10 +4139,10 @@ const RepositoryRoutes = {
3866
4139
  if (assignee === undefined) {
3867
4140
  throw new Error(`Repository command routing has no assignee for "${schema.typeName}".`);
3868
4141
  }
3869
- const customRoute = commandRoutes.get(schema);
3870
- const candidateId = customRoute === undefined
4142
+ const route = commandRoutes.get(schema);
4143
+ const candidateId = route === undefined
3871
4144
  ? RepositoryRoutes.readFirstFieldId(message, schema, "command")
3872
- : RepositoryRoutes.callCommandRoute(customRoute, message, schema, command.context);
4145
+ : await RepositoryRoutes.callCommandRoute(route, message, schema, command.context, reads);
3873
4146
  return Object.freeze({
3874
4147
  entityId: RepositoryRoutes.readRouteId(candidateId, targetIdField, "command"),
3875
4148
  messageFullTypeName: schema.typeName,
@@ -3902,14 +4175,15 @@ const RepositoryRoutes = {
3902
4175
  * @param message Packed Command payload.
3903
4176
  * @param schema Registered Command schema.
3904
4177
  * @param context Source Command context, when present.
4178
+ * @param reads Query access for this routing invocation.
3905
4179
  * @returns Candidate target Entity ID.
3906
4180
  */
3907
- callCommandRoute(route, message, schema, context) {
4181
+ async callCommandRoute(route, message, schema, context, reads) {
3908
4182
  const unpacked = AnyMessages.unpack(message, schema);
3909
4183
  if (unpacked === undefined) {
3910
4184
  throw new Error("Repository command routing requires a readable Command message.");
3911
4185
  }
3912
- return route(unpacked, context ?? create(CommandContextSchema));
4186
+ return await route(unpacked, context ?? create(CommandContextSchema), reads);
3913
4187
  },
3914
4188
  /**
3915
4189
  * Routes an Event to interested Entity IDs using custom or default routing.
@@ -3921,19 +4195,20 @@ const RepositoryRoutes = {
3921
4195
  * @param schemas Accepted Event schemas.
3922
4196
  * @param targetIdField Canonical target Entity ID field.
3923
4197
  * @param eventRoutes Custom Event routes by schema.
4198
+ * @param reads Query access for this routing invocation.
3924
4199
  * @returns Deferred Event route with target IDs.
3925
4200
  */
3926
- routeEvent(event, readiness, commandReactions, schemas, targetIdField, eventRoutes) {
4201
+ async routeEvent(event, readiness, commandReactions, schemas, targetIdField, eventRoutes, reads) {
3927
4202
  const message = event.message;
3928
4203
  if (message === undefined || message.typeUrl === "")
3929
4204
  throw new Error("Repository event routing requires event.message.typeUrl.");
3930
4205
  const schema = RepositoryRoutes.schemaForTypeUrl(schemas, message.typeUrl, "event");
3931
4206
  if (!RepositoryRoutes.hasEventReceiver(schema.typeName, commandReactions, readiness))
3932
4207
  throw new Error(`Repository event routing has no receiver for "${schema.typeName}".`);
3933
- const customRoute = eventRoutes.get(schema);
3934
- const entityIds = customRoute === undefined
4208
+ const route = eventRoutes.get(schema);
4209
+ const entityIds = route === undefined
3935
4210
  ? [RepositoryRoutes.readEventEntityId(event, message, schema, targetIdField)]
3936
- : RepositoryRoutes.callEventRoute(customRoute, message, schema, event.context, targetIdField);
4211
+ : await RepositoryRoutes.callEventRoute(route, message, schema, event.context, targetIdField, reads);
3937
4212
  return Object.freeze({
3938
4213
  entityIds: Object.freeze([...entityIds]),
3939
4214
  messageFullTypeName: schema.typeName,
@@ -4022,19 +4297,21 @@ const RepositoryRoutes = {
4022
4297
  * @param subscriptions State subscribers by source type.
4023
4298
  * @param targetIdField Canonical Projection ID field.
4024
4299
  * @param routes Custom state-update routes by schema.
4300
+ * @param reads Query access for this routing invocation.
4025
4301
  * @returns Route and decoded state, or `undefined` when uninterested.
4026
4302
  */
4027
- routeStateUpdate(event, schemas, subscriptions, targetIdField, routes) {
4028
- const update = RepositoryRoutes.decodeStateUpdate(event, schemas, "Repository state-update routing");
4303
+ async routeStateUpdate(event, schemas, subscriptions, targetIdField, routes, reads) {
4304
+ const operation = "Repository state-update routing";
4305
+ const update = RepositoryRoutes.decodeStateUpdate(event, schemas, operation);
4029
4306
  const candidates = subscriptions.get(update?.schema.typeName ?? "") ?? [];
4030
4307
  const interested = RepositoryRoutes.originSubscribers(candidates, event);
4031
4308
  if (update === undefined || interested.length === 0)
4032
4309
  return undefined;
4033
4310
  const { schema, state } = update;
4034
- const custom = routes.get(schema);
4035
- const candidateIds = custom === undefined
4311
+ const route = routes.get(schema);
4312
+ const candidateIds = route === undefined
4036
4313
  ? [RepositoryRoutes.firstCompatibleId(state, schema, targetIdField, "state update")]
4037
- : RepositoryRoutes.callStateUpdateRoute(custom, state, event.context, targetIdField);
4314
+ : await RepositoryRoutes.callStateUpdateRoute(route, state, event.context, targetIdField, reads);
4038
4315
  return Object.freeze({
4039
4316
  entityIds: Object.freeze([...candidateIds]),
4040
4317
  messageFullTypeName: schema.typeName,
@@ -4087,14 +4364,13 @@ const RepositoryRoutes = {
4087
4364
  * @param state Decoded source Entity state.
4088
4365
  * @param context Source Event context, when present.
4089
4366
  * @param targetIdField Canonical Projection ID field.
4367
+ * @param reads Query access for this routing invocation.
4090
4368
  * @returns Validated Projection target IDs.
4091
4369
  */
4092
- callStateUpdateRoute(route, state, context, targetIdField) {
4093
- const candidates = route(state, context ?? create(EventContextSchema));
4370
+ async callStateUpdateRoute(route, state, context, targetIdField, reads) {
4371
+ const candidates = await route(state, context ?? create(EventContextSchema), reads);
4094
4372
  if (!Array.isArray(candidates))
4095
4373
  throw new Error("Repository state-update routing requires an array of Entity IDs.");
4096
- if (candidates.length > 1_000)
4097
- throw new Error("Repository state-update routing accepts at most 1,000 Entity IDs.");
4098
4374
  const unique = new Map();
4099
4375
  for (const candidate of candidates) {
4100
4376
  const id = RepositoryRoutes.readRouteId(candidate, targetIdField, "state update");
@@ -4102,7 +4378,8 @@ const RepositoryRoutes = {
4102
4378
  if (!unique.has(key))
4103
4379
  unique.set(key, structuredClone(id));
4104
4380
  }
4105
- return Object.freeze([...unique.values()]);
4381
+ const recipients = Object.freeze([...unique.values()]);
4382
+ return recipients;
4106
4383
  },
4107
4384
  /**
4108
4385
  * Calls a custom Event route and validates its target IDs.
@@ -4113,20 +4390,18 @@ const RepositoryRoutes = {
4113
4390
  * @param schema Registered Event schema.
4114
4391
  * @param context Source Event context, when present.
4115
4392
  * @param targetIdField Canonical target Entity ID field.
4393
+ * @param reads Query access for this routing invocation.
4116
4394
  * @returns Validated target Entity IDs.
4117
4395
  */
4118
- callEventRoute(route, message, schema, context, targetIdField) {
4396
+ async callEventRoute(route, message, schema, context, targetIdField, reads) {
4119
4397
  const unpacked = AnyMessages.unpack(message, schema);
4120
4398
  if (unpacked === undefined) {
4121
4399
  throw new Error("Repository event routing requires a readable Event message.");
4122
4400
  }
4123
- const candidates = route(unpacked, context ?? create(EventContextSchema));
4401
+ const candidates = await route(unpacked, context ?? create(EventContextSchema), reads);
4124
4402
  if (!Array.isArray(candidates)) {
4125
4403
  throw new Error("Repository event routing requires an array of Entity IDs.");
4126
4404
  }
4127
- if (candidates.length > 1_000) {
4128
- throw new Error("Repository event routing accepts at most 1,000 Entity IDs.");
4129
- }
4130
4405
  const unique = new Map();
4131
4406
  for (const candidate of candidates) {
4132
4407
  const id = RepositoryRoutes.readRouteId(candidate, targetIdField, "event");
@@ -4134,7 +4409,20 @@ const RepositoryRoutes = {
4134
4409
  if (!unique.has(key))
4135
4410
  unique.set(key, structuredClone(id));
4136
4411
  }
4137
- return Object.freeze([...unique.values()]);
4412
+ const recipients = Object.freeze([...unique.values()]);
4413
+ return recipients;
4414
+ },
4415
+ /**
4416
+ * Records a warning for a large final route without changing its recipients.
4417
+ *
4418
+ * @param signal Signal type selected by the route.
4419
+ * @param repository Receiving repository state type.
4420
+ * @param count Distinct validated recipient count.
4421
+ */
4422
+ warnLargeRoute(signal, repository, count) {
4423
+ if (count <= 1_000)
4424
+ return;
4425
+ emitServerWarning(serverEnvironmentAccess.loggerFor(ServerEnvironment.instance()), `Routing ${signal} to ${String(count)} recipients in repository ${repository}.`, { operation: "repository.route", count, entityType: repository });
4138
4426
  },
4139
4427
  /**
4140
4428
  * Finds a registered message schema by its type URL.
@@ -4405,6 +4693,21 @@ Object.freeze(RepositoryRoutes);
4405
4693
  * Internal repository storage operations.
4406
4694
  */
4407
4695
  const RepositoryStorage = {
4696
+ /**
4697
+ * Opens provider Entity storage for one read scope without a commit handle.
4698
+ *
4699
+ * @typeParam I Entity identifier type.
4700
+ * @typeParam S Generated Entity state type.
4701
+ * @param factory Provider storage factory.
4702
+ * @param input Entity storage location and schema.
4703
+ * @returns The independently closeable Entity storage ports.
4704
+ */
4705
+ openReadEntityStorage(factory, input) {
4706
+ const candidate = factory;
4707
+ if (candidate.createEntityStorage === undefined)
4708
+ throw new Error("StorageFactory does not provide Entity storage for repository reads.");
4709
+ return candidate.createEntityStorage(input);
4710
+ },
4408
4711
  /**
4409
4712
  * Opens Entity storage and an atomic commit port from a storage factory.
4410
4713
  *
@@ -4441,9 +4744,9 @@ const RepositoryStorage = {
4441
4744
  * @returns Shared Entity storage handle.
4442
4745
  */
4443
4746
  openRepositoryEntityStorage(repository, factory, input) {
4444
- const handle = RepositoryStorage.openEntityStorage(factory, input);
4445
4747
  const key = JSON.stringify({ context: input.context, state: input.sourceType.typeName });
4446
4748
  let handles = repositoryEntityHandles.get(repository);
4749
+ const handle = RepositoryStorage.openEntityStorage(factory, input);
4447
4750
  if (handles === undefined) {
4448
4751
  handles = new Map();
4449
4752
  repositoryEntityHandles.set(repository, handles);
@@ -4464,7 +4767,7 @@ const RepositoryStorage = {
4464
4767
  * @returns Entity storage description for the provider.
4465
4768
  */
4466
4769
  entityStorageInput(repository, context) {
4467
- const descriptor = entityStorageDescriptor(context, SpecScanner.scan(repository.entityType));
4770
+ const descriptor = entityStorageDescriptor(context, repositorySpecScanner.scan(repository.entityType, repository.metadata), repository.metadata);
4468
4771
  const history = RepositoryStorage.historyConfiguration(repository);
4469
4772
  return {
4470
4773
  ...descriptor,
@@ -5483,7 +5786,7 @@ const InboxHandoff = {
5483
5786
  * @returns Completion of Inbox receipt and its selected delivery operation.
5484
5787
  */
5485
5788
  async handoffEntityCommand(repository, runtime, command) {
5486
- const route = repository.routeCommand(command);
5789
+ const route = await repository.routeCommand(command);
5487
5790
  const commandId = RepositorySignals.requireCommandId(command);
5488
5791
  const whenReceived = new Date();
5489
5792
  const keepUntil = new Date(whenReceived.getTime() + inboxDedupMs);
@@ -5576,8 +5879,12 @@ const InboxHandoff = {
5576
5879
  storageFactory: runtime.storageFactory,
5577
5880
  strategy: runtime.entityInbox.strategy(),
5578
5881
  });
5579
- const inputs = entityIds.map((entityId) => InboxHandoff.pmEventInboxInput(repository, eventId.value, event, entityId, keepUntil));
5580
- await runtime.entityInbox.receiveAll(delivery, inputs, deliveryTenantId);
5882
+ for (let start = 0; start < entityIds.length; start += 1_000) {
5883
+ const inputs = entityIds
5884
+ .slice(start, start + 1_000)
5885
+ .map((entityId) => InboxHandoff.pmEventInboxInput(repository, eventId.value, event, entityId, keepUntil));
5886
+ await runtime.entityInbox.receiveAll(delivery, inputs, deliveryTenantId);
5887
+ }
5581
5888
  },
5582
5889
  /**
5583
5890
  * Builds a pending Process Manager Event delivery for a single target.
@@ -5653,9 +5960,8 @@ const RepositoryDispatch = {
5653
5960
  const dispatcher = Object.freeze({
5654
5961
  messageSchemas: () => routing.eventSchemas,
5655
5962
  externalEventSchemas: () => routing.externalEventSchemas,
5656
- accept: (event) => {
5657
- accepted.set(event, repository.routeEvent(event));
5658
- return Promise.resolve();
5963
+ accept: async (event) => {
5964
+ accepted.set(event, await repository.routeEvent(event));
5659
5965
  },
5660
5966
  dispatch: (event) => {
5661
5967
  const route = accepted.get(event);
@@ -5681,9 +5987,8 @@ const RepositoryDispatch = {
5681
5987
  const dispatcher = Object.freeze({
5682
5988
  messageSchemas: () => Object.freeze([schema]),
5683
5989
  externalEventSchemas: () => (hasOrigin("external") ? [schema] : []),
5684
- accept: (event) => {
5685
- accepted.set(event, routing.routeStateUpdate(event) ?? null);
5686
- return Promise.resolve();
5990
+ accept: async (event) => {
5991
+ accepted.set(event, (await repositoryAccess.routeStateUpdate(repository, event)) ?? null);
5687
5992
  },
5688
5993
  dispatch: (event) => RepositoryDispatch.dispatchAcceptedState(repository, routing, event, accepted),
5689
5994
  });
@@ -5758,8 +6063,7 @@ const RepositoryDispatch = {
5758
6063
  return (event, rebuild) => {
5759
6064
  const runtime = repositoryRuntimes.get(repository);
5760
6065
  if (runtime === undefined) {
5761
- void repository.routeEvent(event);
5762
- return Promise.resolve();
6066
+ return repository.routeEvent(event).then(() => undefined);
5763
6067
  }
5764
6068
  return new ProjectionEventExecution(repository, routing, runtime, event, rebuild).runDirect();
5765
6069
  };
@@ -5777,10 +6081,10 @@ const RepositoryDispatch = {
5777
6081
  const runtime = repositoryRuntimes.get(repository);
5778
6082
  if (runtime === undefined) {
5779
6083
  if (acceptedRoute === undefined)
5780
- void repository.routeEvent(event);
6084
+ await repository.routeEvent(event);
5781
6085
  return;
5782
6086
  }
5783
- const route = acceptedRoute ?? repository.routeEvent(event);
6087
+ const route = acceptedRoute ?? (await repository.routeEvent(event));
5784
6088
  switch (repository.entityFamily) {
5785
6089
  case "aggregate":
5786
6090
  await RepositoryDispatch.dispatchAggregateEvent(repository, routing, runtime, event, route);
@@ -5822,10 +6126,10 @@ const RepositoryDispatch = {
5822
6126
  const runtime = repositoryRuntimes.get(repository);
5823
6127
  if (runtime === undefined) {
5824
6128
  if (acceptedRoute === undefined)
5825
- void routing.routeStateUpdate(event);
6129
+ await repositoryAccess.routeStateUpdate(repository, event);
5826
6130
  return;
5827
6131
  }
5828
- const route = acceptedRoute ?? routing.routeStateUpdate(event);
6132
+ const route = acceptedRoute ?? (await repositoryAccess.routeStateUpdate(repository, event));
5829
6133
  if (route === undefined)
5830
6134
  return;
5831
6135
  for (const entityId of route.entityIds) {
@@ -5843,7 +6147,7 @@ const RepositoryDispatch = {
5843
6147
  async dispatchRepositoryCommand(repository, routing, command) {
5844
6148
  const runtime = repositoryRuntimes.get(repository);
5845
6149
  if (runtime === undefined) {
5846
- void repository.routeCommand(command);
6150
+ await repository.routeCommand(command);
5847
6151
  return;
5848
6152
  }
5849
6153
  if (repository.entityFamily === "aggregate") {
@@ -5854,7 +6158,7 @@ const RepositoryDispatch = {
5854
6158
  await InboxHandoff.handoffEntityCommand(repository, runtime, command);
5855
6159
  return;
5856
6160
  }
5857
- void repository.routeCommand(command);
6161
+ await repository.routeCommand(command);
5858
6162
  },
5859
6163
  };
5860
6164
  Object.freeze(RepositoryDispatch);