@bimetal/event-sourcing
v0.17.0
Published
Domain-agnostic event-sourcing primitives: store, aggregate, projection, snapshots, middleware. Foundation for @bimetal/calendar-data, @bimetal/table-data and other domain packages.
Maintainers
Readme
@bimetal/event-sourcing
Domain-agnostic event-sourcing primitives. Zero external dependencies, zero @bimetal/* dependencies. Foundation for domain packages like @bimetal/calendar-data and @bimetal/table-data.
Installation
npm install @bimetal/event-sourcingWhat's Inside
Types
DomainEvent<T, P>— immutable, append-only event with metadata (correlationId,userId,source,version)Command<T, P>— input to a dispatch pipelineEventStore— append/read/subscribe interface; storage adapters (store-sqlite,store-eventstoredb, …) implement thisEventBus— in-process pub/subAggregate<S, C, E>—handle(state, command) → events[]+apply(state, event) → stateProjection<R>— derives read models from eventsAggregateState<T>— internal state with per-entity undo stack + processed-command-id windowAggregateSnapshot<T>,SnapshotStore<T>— point-in-time captures for fast hydrationDomainStore<S, C, E, R>— generic facade interface implemented by concrete domain storesDispatchContext<Store>,DispatchMiddleware<Cmd, Evt, Store>— pipeline hooksClock—{ now(): number }time source for event timestamps (was previously in@bimetal/core)EventMetadataProvider— user/source enrichmentDomainError—CONCURRENCY_CONFLICT | STREAM_NOT_FOUND | VALIDATION_ERROR | ENTITY_NOT_FOUND
Errors
DataValidationError— thrown by domain aggregates on invalid commandsConcurrencyError— thrown by anEventStorewhenexpectedVersiondoesn't match
In-Memory Adapters
createInMemoryEventStore()— non-persistedEventStorefor testscreateInMemorySnapshotStore<T>()— non-persistedSnapshotStore<T>createEventBus()—EventBus
Topic-Convention (für Broker-Konsumenten)
eventToTopic(event)— Topic-Convention für@bimetal/brokerund kompatible Adapter. Liefertevent.type. Vollständig deterministisch, keine Mapping-Magie.topicForEventType(type)— Identity-Helper für Subscriptions ohne Event-Objekt.
Da DomainEvent-Types der FQN-Konvention <vendor>.<domain>.<EventName> folgen, funktionieren Pattern-Subscriptions im RabbitMQ-Style direkt: bimetal.# (alle), bimetal.calendar.* (alle Calendar-Events), bimetal.calendar.CalendarEventCreated (exakt).
Bewusst kein Import aus @bimetal/broker — die Verbindung passiert auf der Aufrufer-Seite:
import { eventToTopic } from '@bimetal/event-sourcing';
import { createInProcessBroker } from '@bimetal/broker';
import type { DomainEvent } from '@bimetal/event-sourcing';
const broker = createInProcessBroker<DomainEvent>();
await broker.publish(eventToTopic(event), event);EventStore → Broker Bridge (bridgeEventStoreToBroker)
Eng geschnittene Contract-Utility für die kritische Verbindungsstelle EventStore ↔ Broker.
bridgeEventStoreToBroker(eventStore, broker, options?)— subscribed aufeventStore, ruftbroker.publish(topic, event)pro Event, returntUnsubscribe.- Strukturelle Type-Dependency — kein Import aus
@bimetal/broker. Jeder Adapter, der ein strukturellesPublishTarget<T> = { publish(topic, message): Promise<void> }erfüllt, passt automatisch. - Verantwortlich für: post-append Reihenfolge, async
.catchaufpublish(), sichtbarer Fehlerpfad (Defaultconsole.error, überschreibbar viaonPublishError). - Bewusst nicht zuständig für Retry, Dead-Letter, Backpressure — diese gehören in den
onPublishError-Callback bzw. um die Bridge herum.
import { bridgeEventStoreToBroker } from '@bimetal/event-sourcing';
import { createInProcessBroker } from '@bimetal/broker';
const broker = createInProcessBroker<DomainEvent>();
const off = bridgeEventStoreToBroker(eventStore, broker, {
streamId: 'main', // optional, sonst alle Streams
toTopic: (event) => event.type, // optional, Default = eventToTopic
onPublishError: (err, event, topic) => {
deadLetterQueue.enqueue({ event, topic, error: err });
},
});
// später: off();Use From a Domain Package
import type {
AggregateState,
Command,
DomainEvent,
DomainStore,
Aggregate,
Projection,
} from '@bimetal/event-sourcing';
interface TableRow { readonly id: string; readonly cells: Record<string, unknown>; }
type CreateRow = Command<'CreateRow', { row: TableRow }>;
type RowCreated = DomainEvent<'RowCreated', { row: TableRow }>;
type RowState = AggregateState<TableRow>;
type RowStore = DomainStore<RowState, CreateRow, RowCreated, { rows: readonly TableRow[]; version: number }>;For complete examples, see @bimetal/calendar-data and @bimetal/table-data.
Replaces
This package contains the domain-agnostic portion of the (deprecated) @bimetal/data package, which has been removed. Calendar-specific aggregate/projection/store live in @bimetal/calendar-data.
License
PolyForm Noncommercial License 1.0.0
