AbstractProtectedconstructorProtected Static ReadonlyabstractDeclared for itself, not merely inherited — see Entity.abstractBase.
The guard is Object.hasOwn, so without its own copy this class would walk
straight past it.
ReadonlyidWhether anything inside this boundary is waiting to be published.
Overridden so that it agrees with pullDomainEvents about depth: the
inherited version reads only the root's own buffer, and would report false
for a root whose pending events were all recorded by a child.
Unlike pullDomainEvents, this peeks. It is safe to ask any number of
times and answers the same thing each time.
ProtectedchildThe child entities living inside this boundary.
The root must declare them, because it is the root's job to drain their recorded events. Forget one and its events are silently dropped.
Defaults to none: plenty of aggregate roots are a single entity with no children at all, and that is not a design failure.
Drains this root's events and those of its children, restoring causal order via the sequence stamp.
Without the sort, every child event would appear after every root event,
which would misreport what happened: a CopyDamaged recorded by a Copy
before the root recorded TitleOutOfStock must stay before it.
ProtectedapplyRoute an event to the @Handle method that declares it, then record it.
This is the event-sourced way to change state: the handler performs the mutation, and the event that caused it is kept for publication. State and history cannot drift apart, because one call produces both.
Use Entity.record instead for an event that reports something without changing this aggregate's own state.
ProtectedreplayRoute an event to its handler without recording it.
The distinction from apply is the whole of rehydration: replaying history must rebuild state without re-publishing facts the world already knows. An aggregate loaded from twenty events and then saved would otherwise emit all twenty again.
ProtectedreplayStaticfromRebuild an aggregate from its event stream.
const stock = BookStock.fromEvents(
"dune",
{ title: "Dune", barcodes: [], copies: [] },
await store.read("dune"),
);
initialState is the seed the stream is replayed onto. An event stream
says what changed, never what the empty shape was, so a handler like
this.set("barcodes", [...this.get("barcodes"), ...]) needs an array to
append to before the first event arrives.
It is a RequiredState for the same reason Entity.create's is, and the fit is if anything tighter here: the seed is by definition the shape before anything happened, and an optional property is by convention one that only happens later.
Invariants are asserted once, when the stream has finished — never between events, which would refuse valid histories.
AbstractassertAssert every rule this aggregate is responsible for.
Abstract on purpose: declaring an aggregate root is a claim that you are protecting something, and this method is where you say what. An implementation that is genuinely empty is a signal that the cluster may not need to be an aggregate at all.
Called for you by Entity.mutate when an operation returns, and by fromEvents once a stream has finished replaying. You should not need to call it by hand.
StaticcreateBuild an entity of the class this was called on.
const stock = BookStock.create("dune", {
title: "Dune",
barcodes: [],
copies: [],
}); // : BookStock
Creation takes the entity's required properties only. Supplying an optional one does not compile:
BookStock.create(id, { title: "Dune", barcodes: [], copies: [] }); // ok
BookStock.create(id, { title: "Dune", barcodes: [], copies: [],
author: "Herbert" }); // TS2353
That refusal is the point: an entity begins life holding exactly what it
cannot exist without, and everything else arrives through behaviour that
means something in the domain. Read RequiredState for the
?-means-deferred convention this depends on.
Read state, either whole or one property at a time.
Reading is not a write channel: the value handed back is detached from the
state, so mutating it reaches nothing. The copy is one level deep — see
@domain-toolkit/state for that boundary and its one deliberate limit.
Read state, either whole or one property at a time.
Reading is not a write channel: the value handed back is detached from the
state, so mutating it reaches nothing. The copy is one level deep — see
@domain-toolkit/state for that boundary and its one deliberate limit.
ProtectedsetWrite one property. protected, so state changes belong to the entity's
own behaviour and mean something in the domain.
ProtectedrunOpen a mutation context for the duration of operation.
Separate from mutate because rehydration needs the context without
the invariant check: AggregateRoot.replay reaches set through its
handlers, and a per-event assertion would reject valid histories.
ProtectedassertRefuse a state change made outside a domain operation.
This is what makes mutate mandatory rather than a convention: a change made outside an operation is one nothing will check and nothing can roll back.
ProtectedmutateRun one domain operation atomically: either it completes with this object's invariants intact, or nothing about it happened.
addCopy(barcode: string): void {
this.mutate(() => this.apply(new CopyAdded(barcode)));
}
On the way out it calls assertInvariants. If that throws — or if the operation itself throws — the state is restored and every event the operation recorded is discarded, so a caller who catches the error is telling the truth when they assume nothing happened.
One limit, pinned by a test: rollback covers this object's own state and
event buffer, and does not rewind events recorded by child entities
during the operation. Their buffers are #private to each Entity.
Identity equality. Note what is not compared: none of the attributes.
Two Copy instances loaded separately from the repository, one of them
stale, are still the same copy.
Identity is only meaningful between entities of the same kind: a Copy
and a Member that happen to carry the same string do not refer to the
same thing. "Same kind" is the same concrete class, not instanceof, so
that the answer is symmetric — a.equals(b) and b.equals(a) always
agree, which a subclass-tolerant check cannot promise.
ProtectedrecordAppend an event to this entity's private buffer.
protected on purpose: only the entity's own behaviour may record what
happened to it. Application services cannot fabricate history from outside.
ProtectedsnapshotTake a restorable copy of this entity's state, detached on the way out so it does not change under the operation it is protecting.
protected and deliberately not part of the public surface: this exists for
Entity.mutate, not for callers to take savepoints with.
ProtectedrestoreProtectedmarkHow many events this entity has buffered — a mark to rewind to.
Protectedrewind
An Aggregate Root is an Entity with a second job: it is the consistency boundary for a cluster of objects that must obey a rule together.
Every Aggregate Root is an Entity. Not every Entity is an Aggregate Root. The extra responsibilities are exactly three:
How big should an aggregate be?
As small as its invariants allow. Two objects belong in the same aggregate only if there is a rule that must be true of both of them at every instant. If the rule can be true "shortly afterwards" — eventually — then they are two aggregates and an event connects them.
In this codebase:
BookStockcontains itsCopyentities becauseavailableCountmust match the copies at every instant.MemberandLoanare separate aggregates, because "a member has at most N loans" is allowed to be repaired a moment later, by an event handler.