Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
# OpenSpec Change: enterprise-event-sourcing
- **Change ID**: enterprise-event-sourcing
- **Package**: `@suaveplan/event-sourcing`
- **Category**: universal
- **Target path**: `universal/events/event-sourcing`
- **Status**: PROPOSED
- **Date**: 2026-04-24
---
## Summary
Deliver `@suaveplan/event-sourcing` on the genesis branch from scratch at enterprise quality. The package provides event-sourcing patterns for building aggregate-driven domain models: a typed `Aggregate` base class with `apply`/`raise` lifecycle, an `EventStore` interface with in-memory and pluggable-adapter implementations, optimistic concurrency via version vectors, a `Projection` builder for read-model construction, snapshotting support, `@suaveplan/diff` delta integration for efficient event payload storage, `@suaveplan/events` integration for publish-after-commit, structured errors, and OTel instrumentation.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/events` (`workspace:^`) | Publish committed domain events to subscribers after persistence |
| `@suaveplan/diff` (`workspace:^`) | JSON Patch delta encoding for efficient event payload storage and upcasting |
| `@suaveplan/error` (`workspace:^`) | Base class for `EventSourcingError`; `defineCodes` for `ES.*` codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `IdGenerator`, `EventBusLike` injection interfaces |
| `zod` (`catalog:forms`) | Zod schemas for event envelope, snapshot, and options types |
| `@opentelemetry/api` (`catalog:observability`) | Spans, counters, histograms on append, load, project operations |
| `tslib` (`catalog:build`) | Runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
---
## Public API surface
### Domain event
```ts
interface DomainEvent<TPayload = unknown> {
id: string
aggregateId: string
aggregateType: string
type: string
version: number // sequence number within the aggregate stream
payload: TPayload
metadata: EventMetadata
occurredAt: Date
}
interface EventMetadata {
causationId?: string
correlationId?: string
actor?: string
[key: string]: unknown
}
```
### Aggregate base class
```ts
abstract class Aggregate<TState> {
readonly id: string
protected state: TState
readonly version: number
// Raise a domain event — applies it immediately and queues it for commit
protected raise<TPayload>(type: string, payload: TPayload): void
// Apply a domain event to internal state (called by replay too)
protected abstract apply(event: DomainEvent): void
// Pending events not yet committed
readonly pendingEvents: DomainEvent[]
// Replay a sequence of historical events to rebuild state
replayFrom(events: DomainEvent[]): void
// Mark pending events as committed (called by EventStore after write)
clearPendingEvents(): void
}
```
### EventStore interface
```ts
interface EventStore {
append(events: DomainEvent[], expectedVersion: number): Promise<void>
load(aggregateId: string, fromVersion?: number): Promise<DomainEvent[]>
loadSnapshot(aggregateId: string): Promise<Snapshot | undefined>
saveSnapshot(snapshot: Snapshot): Promise<void>
getStream(aggregateId: string): AsyncGenerator<DomainEvent>
}
interface Snapshot {
aggregateId: string
aggregateType: string
version: number
state: unknown
takenAt: Date
}
```
### In-memory event store
```ts
function createMemoryEventStore(options?: MemoryEventStoreOptions): EventStore
interface MemoryEventStoreOptions {
maxStreamLength?: number
clock?: Clock
idGenerator?: IdGenerator
bus?: EventBusLike // publishes committed events to subscribers
logger?: LoggerLike
}
```
### Aggregate repository
```ts
// Combines EventStore with Aggregate reconstruction
function createAggregateRepository<T extends Aggregate<unknown>>(
options: AggregateRepositoryOptions<T>
): AggregateRepository<T>
interface AggregateRepository<T extends Aggregate<unknown>> {
load(aggregateId: string): Promise<T>
save(aggregate: T): Promise<void>
exists(aggregateId: string): Promise<boolean>
}
interface AggregateRepositoryOptions<T extends Aggregate<unknown>> {
store: EventStore
factory: (id: string) => T
snapshotThreshold?: number // take snapshot every N events; default: disabled
}
```
### Projection builder
```ts
function createProjection<TState>(options: ProjectionOptions<TState>): Projection<TState>
interface ProjectionOptions<TState> {
initialState: TState
handlers: ProjectionHandlers<TState>
}
type ProjectionHandlers<TState> = Record<
string,
(state: TState, event: DomainEvent) => TState
>
interface Projection<TState> {
project(events: DomainEvent[]): TState
projectStream(stream: AsyncIterable<DomainEvent>): Promise<TState>
}
```
### Upcasting (event schema migration)
```ts
// Register an upcaster that migrates payloads from old event versions
function registerUpcaster(options: UpcasterOptions): void
interface UpcasterOptions {
eventType: string
fromVersion: number
toVersion: number
upcast: (payload: unknown) => unknown
}
// Upcast a sequence of events to the latest schema version
function upcastEvents(events: DomainEvent[]): DomainEvent[]
```
### Error handling
```ts
class EventSourcingError extends SuaveplanError<EventSourcingErrorCode>
type EventSourcingErrorCode =
| "ES_CONCURRENCY_CONFLICT"
| "ES_AGGREGATE_NOT_FOUND"
| "ES_SNAPSHOT_INVALID"
| "ES_UPCASTER_MISSING"
| "ES_STREAM_CORRUPT"
| "ES_APPEND_FAILED"
function registerEventSourcingErrorCodes(): void
```
### Zod schemas
```ts
const DomainEventSchema: ZodSchema
const SnapshotSchema: ZodSchema
const MemoryEventStoreOptionsSchema: ZodSchema
const AggregateRepositoryOptionsSchema: ZodSchema
```
---
## New additions for genesis
### 1. Optimistic concurrency
`EventStore.append` takes `expectedVersion`. If the current stream version does not match, it throws `EventSourcingError("ES_CONCURRENCY_CONFLICT")`. The `AggregateRepository` passes `aggregate.version` as the expected version, retrying the load-modify-save cycle up to the caller-configured `maxRetries`.
### 2. Snapshot-based reconstruction
When `snapshotThreshold` is set, `AggregateRepository.save` automatically saves a snapshot when `aggregate.version % snapshotThreshold === 0`. On `load`, the repository loads the latest snapshot first, then replays only events after the snapshot version.
### 3. `@suaveplan/diff` delta payloads
The in-memory store optionally stores event payloads as JSON Patch deltas against the previous event in the same stream (using `@suaveplan/diff`). `EventStore.load` transparently upcasts deltas back to full payloads. This is an opt-in compression mode controlled by `MemoryEventStoreOptions.deltaPayloads`.
### 4. `@suaveplan/events` publish-after-commit
When `bus` is provided in `MemoryEventStoreOptions`, the store emits each committed `DomainEvent` onto the bus as `domain-event:<aggregateType>:<eventType>` after a successful `append`. This enables subscriptions without polling the store.
### 5. OTel instrumentation
- `store.append` — span `es.append`, attributes: `es.aggregate_id`, `es.aggregate_type`, `es.event_count`, `es.version`
- `store.load` — span `es.load`, attributes: `es.aggregate_id`, `es.event_count`
- `repository.save` — span `es.repository.save`, includes optimistic concurrency retry count
- Counters: `es.events_appended_total`, `es.concurrency_conflicts_total`, `es.snapshots_taken_total`
- Histogram: `es.append_duration_ms`, `es.load_duration_ms`
### 6. Property-based tests (fast-check)
- Replay determinism: replaying the same event sequence always produces the same aggregate state
- Optimistic concurrency: concurrent appends with the same expected version produce exactly one success
- Projection completeness: projecting all events returns the same state as building incrementally
- Snapshot round-trip: saving and loading a snapshot yields identical aggregate state
### 7. Benchmark suite (`src/__benchmarks__/`)
- Load aggregate with 100, 1k, 10k events (no snapshot)
- Load aggregate with 10k events and snapshot at 500
- Append 1k events to a single stream
---
## File layout
```text
universal/events/event-sourcing/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
common.ts
common.md
core/
aggregate/
aggregate.ts
aggregate.md
aggregate.types.ts
aggregate.test.ts
index.ts
event-store/
event-store.ts
event-store.md
event-store.types.ts
event-store.test.ts
index.ts
memory-event-store/
memory-event-store.ts
memory-event-store.md
memory-event-store.types.ts
memory-event-store.test.ts
index.ts
aggregate-repository/
aggregate-repository.ts
aggregate-repository.md
aggregate-repository.types.ts
aggregate-repository.test.ts
index.ts
projection/
projection-builder/
projection-builder.ts
projection-builder.md
projection-builder.types.ts
projection-builder.test.ts
index.ts
upcasting/
upcaster/
upcaster.ts
upcaster.md
upcaster.types.ts
upcaster.test.ts
index.ts
__benchmarks__/
event-sourcing.bench.ts
tests/
integration.test.ts
property.test.ts
```
---
## Impact
**Downstream packages**: any domain-model package following event-sourcing architecture. Enables CQRS read-model construction via projections and cross-context publishing via `@suaveplan/events`.
**Workspace dep prerequisites**: `@suaveplan/events`, `@suaveplan/diff`, `@suaveplan/error`, `@suaveplan/types` must be published to Verdaccio.
**Breaking changes**: none (new package).

View File

@@ -0,0 +1,99 @@
# Tasks: enterprise-event-sourcing
- **Change ID**: enterprise-event-sourcing
- **Package**: `@suaveplan/event-sourcing`
- **Date**: 2026-04-24
## Prerequisites (must be published to Verdaccio first)
- [x] `@suaveplan/events`
- [x] `@suaveplan/diff`
- [x] `@suaveplan/error`
- [x] `@suaveplan/types`
## Tasks
### 1. Scaffold package
- [x] Create `universal/events/event-sourcing/` directory tree per file layout in `proposal.md`
- [x] Write `package.json` (name `@suaveplan/event-sourcing`, version `0.1.0`, `sideEffects: false`, correct workspace deps and catalog externals)
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`
### 2. Error codes (`src/error-codes.ts`)
- [x] Define `EventSourcingErrorCode` union type
- [x] Call `defineCodes` to register all `ES.*` codes
- [x] Export `registerEventSourcingErrorCodes()` and `EventSourcingError`
### 3. Types (`src/types/common.ts`)
- [x] Define and export `DomainEvent<T>`, `EventMetadata`, `Snapshot`, `EventStore`, `AggregateRepositoryOptions`
- [x] Write `common.md`
### 4. `Aggregate` base class (`src/core/aggregate/`)
- [x] Implement abstract `Aggregate<TState>` with `raise`, `apply` (abstract), `replayFrom`, `clearPendingEvents`, `pendingEvents`, `version`, `id`
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
### 5. `EventStore` interface (`src/core/event-store/`)
- [x] Define and export `EventStore` interface with `append`, `load`, `loadSnapshot`, `saveSnapshot`, `getStream`
- [x] Write `.md`, `.types.ts`, `index.ts`
### 6. In-memory event store (`src/core/memory-event-store/`)
- [x] Implement `createMemoryEventStore` with:
- Per-aggregate stream storage (Map of arrays)
- Optimistic concurrency: `append` checks `expectedVersion` and throws `ES_CONCURRENCY_CONFLICT` on mismatch
- Snapshot storage alongside event streams
- Publish committed events to `@suaveplan/events` bus after successful `append`
- Optional delta-payload compression via `@suaveplan/diff` when `deltaPayloads: true`
- OTel spans, counters, histograms per `proposal.md` §5
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
### 7. Aggregate repository (`src/core/aggregate-repository/`)
- [x] Implement `createAggregateRepository<T>` with snapshot-based reconstruction
- [x] Optimistic concurrency retry loop (configurable `maxRetries`)
- [x] Auto-snapshot at configurable `snapshotThreshold`
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
### 8. Projection builder (`src/projection/projection-builder/`)
- [x] Implement `createProjection<TState>` with `project(events)` and `projectStream(stream)` methods
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
### 9. Upcaster registry (`src/upcasting/upcaster/`)
- [x] Implement `registerUpcaster` and `upcastEvents` with version-ordered upcaster chain
- [x] Write `.md`, `.types.ts`, `.test.ts`, `index.ts`
### 10. Tests
- [x] Unit tests co-located (100% coverage target)
- [x] `tests/integration.test.ts` — full aggregate lifecycle: create, load, save with snapshot, reload from snapshot + trailing events
- [x] `tests/property.test.ts` — fast-check properties per `proposal.md` §6
### 11. Benchmarks (`src/__benchmarks__/event-sourcing.bench.ts`)
- [x] Implement benchmark cases per `proposal.md` §7
### 12. Docs
- [x] Every `.ts` implementation file has a sibling `.md`
- [x] Root `README.md`
### 13. Pre-publish gate
- [x] `bunx turbo lint typecheck test --filter=@suaveplan/event-sourcing` green
- [x] `bunx turbo test:coverage --filter=@suaveplan/event-sourcing` ≥ 100% on touched modules
### 14. Publish
- [x] `bun scripts/publish.ts --package @suaveplan/event-sourcing`
### 15. Archive
- [x] Fold spec delta into `openspec/specs/events/spec.md`
- [x] `mv openspec/changes/enterprise-event-sourcing openspec/archive/2026-04-24-enterprise-event-sourcing`