@spine-event-engine/server 2.0.0-snapshot.12 → 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.
- package/README.md +18 -0
- package/REFERENCE.md +93 -7
- package/dist/entity/entity-transaction.d.ts +46 -42
- package/dist/entity/entity-transaction.d.ts.map +1 -1
- package/dist/entity/entity-transaction.js +130 -35
- package/dist/entity/entity-transaction.js.map +1 -1
- package/dist/entity/entity.d.ts +132 -84
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +213 -217
- package/dist/entity/entity.js.map +1 -1
- package/dist/handler/event-registration-readiness.d.ts +13 -56
- package/dist/handler/event-registration-readiness.d.ts.map +1 -1
- package/dist/handler/event-registration-readiness.js +23 -43
- package/dist/handler/event-registration-readiness.js.map +1 -1
- package/dist/handler/generated-handler-registry.d.ts +8 -0
- package/dist/handler/generated-handler-registry.d.ts.map +1 -1
- package/dist/handler/generated-handler-registry.js +165 -3
- package/dist/handler/generated-handler-registry.js.map +1 -1
- package/dist/handler/handler-decorators.d.ts +70 -33
- package/dist/handler/handler-decorators.d.ts.map +1 -1
- package/dist/handler/handler-decorators.js +88 -31
- package/dist/handler/handler-decorators.js.map +1 -1
- package/dist/handler/handler-metadata.d.ts +53 -51
- package/dist/handler/handler-metadata.d.ts.map +1 -1
- package/dist/handler/handler-metadata.js +214 -42
- package/dist/handler/handler-metadata.js.map +1 -1
- package/dist/handler/registration-readiness-metadata.d.ts +4 -0
- package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
- package/dist/handler/registration-readiness-metadata.js +82 -1
- package/dist/handler/registration-readiness-metadata.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/repository/repository.d.ts +137 -20
- package/dist/repository/repository.d.ts.map +1 -1
- package/dist/repository/repository.js +2589 -282
- package/dist/repository/repository.js.map +1 -1
- package/dist/runtime/standalone-handler-runtime.d.ts +16 -0
- package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
- package/dist/runtime/standalone-handler-runtime.js +93 -18
- package/dist/runtime/standalone-handler-runtime.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -13
|
@@ -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
|
|
88
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
392
|
-
|
|
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
|
-
*
|
|
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,
|
|
438
|
-
const committed = await this.persistAggregateUpdate(loaded, entityId,
|
|
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?.
|
|
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
|
|
687
|
+
produced = await this.#invokeAssignee(loaded.entity, intake.assignee.handler, intake.message, EntityInvocation.commandHandlerContext(this.#command));
|
|
507
688
|
}
|
|
508
689
|
catch (error) {
|
|
509
|
-
|
|
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.#
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
775
|
+
return Object.freeze(produced.map((signal) => this.#bindProducedEvent(signal, entityId, dispatchVersion)));
|
|
558
776
|
}
|
|
559
|
-
|
|
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:
|
|
787
|
+
version: version.number,
|
|
562
788
|
});
|
|
563
|
-
const bound =
|
|
564
|
-
|
|
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
|
-
|
|
620
|
-
await this.#
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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:
|
|
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 =
|
|
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,
|
|
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
|
|
774
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
848
|
-
const
|
|
849
|
-
|
|
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
|
|
912
|
-
const
|
|
913
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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),
|
|
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
|
|
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
|
|
1684
|
+
return signals;
|
|
1090
1685
|
}
|
|
1091
1686
|
catch (error) {
|
|
1092
1687
|
transactionalEntityAccess.rollback(entity);
|
|
1093
1688
|
throw error;
|
|
1094
1689
|
}
|
|
1095
1690
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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(
|
|
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),
|
|
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
|
-
|
|
1965
|
+
const signals = this.#support.normalizeProducedSignals(produced);
|
|
1966
|
+
RepositoryHandlers.requireDeclaredOutputs(handler.handler, signals);
|
|
1967
|
+
output.push(...signals);
|
|
1262
1968
|
}
|
|
1263
1969
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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([
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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.
|