@ebca/core
v0.0.12
Published
Core runtime for Event-Based Component Architecture.
Maintainers
Readme
@ebca/core
Core runtime for Event-Based Component Architecture.
@ebca/core gives a NestJS backend a typed component lifecycle: entities hold components, commands are components, systems react to lifecycle events, and all state changes go through ComponentManager.
It is the foundation package for EBCA. It does not include WebSocket, GraphQL, REST, or application-specific business logic.
Install
npm install @ebca/corePeer dependencies are intentionally explicit: NestJS, NATS, TypeORM, cache-manager, Redis/Keyv, RxJS, and reflect-metadata are supplied by the consuming application.
What It Provides
- Base classes:
BaseEntity,BaseComponent,BaseCommandComponent. - Runtime decorators:
@Entity,@Component,@System,@EbcaPattern. - Read-side decorators:
@EbcaReadRepository,@EbcaQuery,@EbcaQueryParam. - IO metadata:
@EbcaIOfor runtime architecture reports. - Inbound component metadata for gateway operations, ownership, roles, and fields.
- Projection metadata for WebSocket and GraphQL component snapshots/subscriptions.
- Persistence mapping:
@PersistentProperty. ComponentManageras the single component read/write path.- TypeORM JSONB persistence and column projection support.
- NATS lifecycle publication.
- Delayed streams and opt-in ordered ingress for command paths.
Mental Model
flowchart LR
Boundary["Boundary writes command component"] --> Manager["ComponentManager"]
Manager --> State["Redis / DB projection"]
Manager --> Lifecycle["ebca.entity.id.event.component"]
Lifecycle --> System["@EbcaPattern handler"]
System --> ManagerCommands, facts, inputs, and state are all represented as components. A gateway or API writes an intent component. A system owns the domain decision. The domain result is expressed as more component lifecycle, not as hidden side effects.
Example
Use the runnable example in examples/counter as the canonical minimal app.
It shows the real core surface:
CounterEntityextendsBaseEntityand is registered with@Entity.IncrementCounterCommandComponentextendsBaseCommandComponent.CounterValueComponentis a persistentBaseComponent.CounterSystemsubscribes with object-form@EbcaPattern.@ebca/rest-gatewayexposes REST mutations, Swagger, and read queries over the sameComponentManagerpath.
The example includes the required NestJS wiring for PostgreSQL, Redis, NATS, and EbcaModule.
Command components track their source (system, websocket, rest, or graphql) so gateways can share the same command base without hiding where an intent entered the system.
Read query transport result contracts are inferred from the method return type when the CLI can read the repository source file. @EbcaQuery({ resultType }) remains an override for unusual signatures, but normal async readWorldMap(): Promise<MapMarkupDocument> methods need no extra DTO or declaration layer. Queries without explicit gates are available to every query transport; use a non-empty gates list when a query should be transport-specific, or gates: [] when it is repository-internal only.
Command Storage
Command components are transient by default. ComponentManager emits their lifecycle event and does not keep the command in Redis, so repeated writes behave like repeated intent events.
For idempotent paths, opt in per command component:
@Component({
transient: false,
inbound: {
expose: true,
fields: ['commandId', 'orderId', 'amount'],
},
})
export class PlaceOrderCommandComponent extends BaseCommandComponent {
readonly orderId!: string;
readonly amount!: number;
}transient: false is not JSONB persistence. It stores the current command component in the entity Redis hash, keeps getComponent and hasComponent meaningful for that command, and preserves terminal status when a system calls updateComponent after succeed() or reject(...).
Duplicate add or upsert with the same commandId and same intent payload is ignored. The same commandId with a different intent payload is rejected. A different command instance for the same entity and component name requires explicit removal first, or a different command/order entity if the domain needs multiple command instances.
REST and GraphQL gateways create a random commandId when one is missing. Real idempotent retries need a stable caller-provided commandId exposed through inbound.fields.
Runtime Metadata
EBCA decorators register metadata at runtime. That metadata powers:
- handler discovery;
- architecture reports;
- command workflow graphs;
- IO coverage checks;
- generated transport contracts;
- optional WebSocket and GraphQL adapters.
This is the heart of why EBCA works well with AI-assisted development: the system can explain its own shape before a human or agent edits it.
Ordered Ingress
EBCA can keep default NATS lifecycle behavior for most commands while enabling ordered ingress only where sequence matters.
When a handler pattern opts in, command publication resolves a shard at publish time, writes a JetStream envelope, replays the original EBCA topic through the partition consumer, and acknowledges only after the handler completes.
This avoids global locks and keeps horizontal scaling available.
Boundaries
@ebca/core should stay domain-agnostic.
- Put business rules in systems.
- Put transport policy in adapters.
- Put read-side filtering in repositories.
- Use
ComponentManagerfor state changes. - Keep direct DB writes out of domain lifecycle code unless they are explicit read-model/projection concerns.
License
Apache-2.0.
