Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added `@OperationTarget()` and `@OperationFor(...)` annotations in `continuum` to support operation-driven mutation without requiring `AggregateRoot`.

### Changed

- `continuum_generator` now discovers operation targets via `@OperationTarget()` (and still supports `AggregateRoot` as a legacy marker).
- `continuum_lints` now applies missing-handler and missing-creation-factory checks to `@OperationTarget()` classes.
- `EventSourcingStore` and `StateBasedStore` now accept `targets:` instead of `aggregates:` (with `aggregates:` kept as a deprecated alias).
- State-based persistence now prefers `TargetPersistenceAdapter<T>`; `AggregatePersistenceAdapter<T>` remains as a deprecated alias for backward compatibility.

### Fixed

- Example `abstract_interface_targets.dart` is now discoverable by codegen by marking its target types with `@OperationTarget()`.

### Deprecated

- Deprecated `@AggregateEvent(...)` in favor of operation-oriented annotations.

## [5.1.0] - 2026-02-20

### Added
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ Continuum provides a comprehensive event sourcing framework for Dart application
| Layer | Package | Purpose |
|-------|---------|---------|
| 0 | **`continuum`** | Core types: annotations, events, identity, dispatch registries |
| 0 | **`continuum_generator`** | Code generator for aggregate and event boilerplate |
| 0 | **`continuum_generator`** | Code generator for operation and event boilerplate |
| 1 | **`continuum_uow`** | Unit of Work session engine: sessions, transactional runner, commit handler |
| 2 | **`continuum_event_sourcing`** | Event sourcing persistence: event stores, serialization, projections |
| 2 | **`continuum_state`** | State-based persistence: REST/DB adapter-driven aggregate persistence |
| 2 | **`continuum_state`** | State-based persistence: REST/DB adapter-driven target persistence |
| 3 | **`continuum_store_memory`** | In-memory EventStore for testing |
| 3 | **`continuum_store_hive`** | Hive-backed EventStore for local persistence |
| 3 | **`continuum_store_sembast`** | Sembast-backed EventStore for cross-platform persistence |
Expand Down Expand Up @@ -70,10 +70,12 @@ import 'package:continuum/continuum.dart';

part 'shopping_cart.g.dart';

class ShoppingCart extends AggregateRoot<String> with _$ShoppingCartEventHandlers {
@OperationTarget()
class ShoppingCart with _$ShoppingCartEventHandlers {
String id;
List<String> items;

ShoppingCart._({required super.id, required this.items});
ShoppingCart._({required this.id, required this.items});
static ShoppingCart createFromCartCreated(CartCreated event) {
return ShoppingCart._(id: event.cartId, items: []);
}
Expand All @@ -84,7 +86,7 @@ class ShoppingCart extends AggregateRoot<String> with _$ShoppingCartEventHandler
}
}

@AggregateEvent(of: ShoppingCart, type: 'cart.created', creation: true)
@OperationFor(type: ShoppingCart, key: 'cart.created', creation: true)
class CartCreated implements ContinuumEvent {
final String cartId;

Expand Down Expand Up @@ -121,7 +123,7 @@ import 'continuum.g.dart';

final store = EventSourcingStore(
eventStore: InMemoryEventStore(),
aggregates: $aggregateList,
targets: $aggregateList,
);

final session = store.openSession();
Expand All @@ -144,7 +146,7 @@ import 'continuum.g.dart';

final store = StateBasedStore(
adapters: {ShoppingCart: CartApiAdapter(httpClient)},
aggregates: $aggregateList,
targets: $aggregateList,
);

final session = store.openSession();
Expand All @@ -159,7 +161,7 @@ await session.saveChangesAsync(); // Adapter persists to backend

### continuum

Core library providing annotations (`@Aggregate`, `@AggregateEvent`, `@Projection`), event contracts (`ContinuumEvent`), identity types (`EventId`, `StreamId`), dispatch registries, `EventApplicationMode`, and core exceptions.
Core library providing annotations (`@OperationTarget`, `@OperationFor`, `@Projection`), event contracts (`ContinuumEvent`), identity types (`EventId`, `StreamId`), dispatch registries, `EventApplicationMode`, and core exceptions.

### continuum_generator

Expand Down Expand Up @@ -191,7 +193,7 @@ Sembast-backed `EventStore` implementation for cross-platform local persistence.

### continuum_lints

Custom lint rules for `@Aggregate` and `@Projection` classes.
Custom lint rules for operation targets and projections.

## License

Expand Down
4 changes: 2 additions & 2 deletions doc/draft/extract-unit-of-work-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Layer 3 ─ Store implementations

| Layer | Package | Contains |
|---|---|---|
| 0 | `continuum` | `Operation`, `ContinuumEvent`, `@AggregateEvent`, `@Projection`, `AggregateFactoryRegistry`, `EventApplierRegistry`, `EventApplicationMode`, `StreamId`, `GeneratedAggregate`, codegen annotations, all operation/event base contracts |
| 0 | `continuum` | `Operation`, `ContinuumEvent`, `@OperationTarget`, `@OperationFor`, `@Projection`, `AggregateFactoryRegistry`, `EventApplierRegistry`, `EventApplicationMode`, `StreamId`, `GeneratedAggregate`, codegen annotations, all operation/event base contracts |
| 1 | `continuum_uow` | `Session`, `SessionBase`, `TrackedEntity`, `SessionStore`, `TransactionalRunner`, `CommitHandler`, `ConcurrencyException`, `InvalidOperationException`, `UnsupportedOperationException`, `PartialSaveException` |
| 2a | `continuum_es` | `EventSourcingStore`, `SessionImpl`, `EventStore`, `AtomicEventStore`, `EventSerializer`, `EventSerializerRegistry`, `JsonEventSerializer`, `StoredEvent`, `ExpectedVersion`, `StreamAppendBatch`, `ProjectionEventStore`, all projection types |
| 2b | `continuum_state` | `StateBasedStore`, `StateBasedSession`, `AggregatePersistenceAdapter`, `PermanentAdapterException`, `TransientAdapterException` |
Expand Down Expand Up @@ -454,7 +454,7 @@ These are Continuum core — the mutation framework primitives:

- `Operation` marker interface
- `ContinuumEvent` (`implements Operation` + `BoundedDomainEvent` integration)
- `@AggregateEvent`, `@Projection` annotations
- `@OperationTarget`, `@OperationFor`, `@Projection` annotations
- `AggregateFactoryRegistry`, `EventApplierRegistry`
- `EventApplicationMode`
- `StreamId`
Expand Down
6 changes: 3 additions & 3 deletions doc/draft/session-store-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,14 @@ This is a **store-level or session-level configuration**, not a per-event decisi
// Eager (default) — events mutate the aggregate immediately
final store = EventSourcingStore(
eventStore: SembastEventStore(database),
aggregates: [$User, $Playlist],
targets: [$User, $Playlist],
applicationMode: EventApplicationMode.eager,
);

// Deferred — events are recorded but applied only on successful save
final store = EventSourcingStore(
eventStore: SembastEventStore(database),
aggregates: [$User, $Playlist],
targets: [$User, $Playlist],
applicationMode: EventApplicationMode.deferred,
);
```
Expand Down Expand Up @@ -1064,7 +1064,7 @@ State-Based: applyAsync → save to Backend → commitHandler → proj
// main.dart
final store = EventSourcingStore(
eventStore: SembastEventStore(database),
aggregates: [$AudioFile, $Playlist],
targets: [$AudioFile, $Playlist],
);

final runner = TransactionalRunner(
Expand Down
14 changes: 8 additions & 6 deletions packages/continuum/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dev_dependencies:

## What This Package Provides

- **Annotations**: `@AggregateEvent()`, `@Projection()`
- **Annotations**: `@OperationTarget()`, `@OperationFor(...)`, `@Projection()`
- **Event contract**: `ContinuumEvent` interface
- **Identity types**: `EventId`, `StreamId`
- **Operation enum**: `Operation.create`, `Operation.mutate`
Expand Down Expand Up @@ -48,18 +48,20 @@ Continuum is organized into four layers:

## Quick Start

### Define Your Aggregate
### Define Your Target

```dart
import 'package:continuum/continuum.dart';

part 'user.g.dart';

class User extends AggregateRoot<String> with _$UserEventHandlers {
@OperationTarget()
class User with _$UserEventHandlers {
String id;
String name;
String email;

User._({required super.id, required this.name, required this.email});
User._({required this.id, required this.name, required this.email});

static User createFromUserRegistered(UserRegistered event) {
return User._(id: event.userId, name: event.name, email: event.email);
Expand All @@ -77,7 +79,7 @@ class User extends AggregateRoot<String> with _$UserEventHandlers {
```dart
import 'package:continuum/continuum.dart';

@AggregateEvent(of: User, type: 'user.registered', creation: true)
@OperationFor(type: User, key: 'user.registered', creation: true)
class UserRegistered implements ContinuumEvent {
UserRegistered({
required this.userId,
Expand Down Expand Up @@ -111,7 +113,7 @@ dart run build_runner build

This creates:
- `user.g.dart` with `_$UserEventHandlers` mixin
- `lib/continuum.g.dart` with `$aggregateList`
- `lib/continuum.g.dart` with `$aggregateList` (auto-discovered operation targets)

## Custom Lints (Recommended)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
/// Example: Abstract and Interface Aggregates
/// Example: Abstract and Interface Targets
///
/// This example demonstrates that Continuum can generate event handlers and
/// dispatch logic for aggregates declared as an `abstract class` or an
/// dispatch logic for targets declared as an `abstract class` or an
/// `interface class`.
library;

import 'package:bounded/bounded.dart';
import 'package:continuum/continuum.dart';

part 'abstract_interface_aggregates.g.dart';
part 'abstract_interface_targets.g.dart';

void main() {
print('═══════════════════════════════════════════════════════════════════');
print('Example: Abstract and Interface Aggregates');
print('Example: Abstract and Interface Targets');
print('═══════════════════════════════════════════════════════════════════');
print('');

_runAbstractAggregateExample();
_runAbstractTargetExample();
print('');
_runInterfaceAggregateExample();
_runInterfaceTargetExample();
}

void _runAbstractAggregateExample() {
print('ABSTRACT AGGREGATE');
void _runAbstractTargetExample() {
print('ABSTRACT TARGET');

final user = AbstractUser(
id: const AbstractUserId('abstract-user-1'),
Expand All @@ -42,8 +42,8 @@ void _runAbstractAggregateExample() {
print(' ✓ Event dispatch works via AbstractUserBase');
}

void _runInterfaceAggregateExample() {
print('CONCRETE AGGREGATE');
void _runInterfaceTargetExample() {
print('CONCRETE TARGET');

final user = UserContract(
id: const UserContractId('contract-user-1'),
Expand All @@ -66,23 +66,25 @@ final class AbstractUserId extends TypedIdentity<String> {
const AbstractUserId(super.value);
}

/// An abstract aggregate base type.
/// An abstract target base type.
///
/// The generator produces:
/// - `mixin _$AbstractUserBaseEventHandlers`
/// - `extension $AbstractUserBaseEventDispatch on AbstractUserBase`
abstract class AbstractUserBase extends AggregateRoot<AbstractUserId> with _$AbstractUserBaseEventHandlers {
@OperationTarget()
abstract class AbstractUserBase with _$AbstractUserBaseEventHandlers {
AbstractUserBase({
required AbstractUserId id,
required this.id,
required this.email,
required this.name,
}) : super(id);
});

final AbstractUserId id;
String email;
final String name;
}

/// A concrete implementation of the abstract aggregate.
/// A concrete implementation of the abstract target.
class AbstractUser extends AbstractUserBase {
AbstractUser({
required super.id,
Expand All @@ -100,12 +102,15 @@ final class UserContractId extends TypedIdentity<String> {
const UserContractId(super.value);
}

/// A concrete implementation of the interface aggregate.
class UserContract extends AggregateRoot<UserContractId> with _$UserContractEventHandlers {
/// A concrete implementation of the interface target.
@OperationTarget()
class UserContract with _$UserContractEventHandlers {
UserContract({
required UserContractId id,
required this.id,
required this.displayName,
}) : super(id);
});

final UserContractId id;

String displayName;

Expand All @@ -116,7 +121,7 @@ class UserContract extends AggregateRoot<UserContractId> with _$UserContractEven
}

/// Event that changes an abstract user's email.
@AggregateEvent(of: AbstractUserBase, type: 'example.abstract_user.email_changed')
@OperationFor(type: AbstractUserBase, key: 'example.abstract_user.email_changed')
class AbstractUserEmailChanged implements ContinuumEvent {
AbstractUserEmailChanged({
required this.newEmail,
Expand Down Expand Up @@ -155,8 +160,8 @@ class AbstractUserEmailChanged implements ContinuumEvent {
};
}

/// Event that renames a user implementing an interface aggregate.
@AggregateEvent(of: UserContract, type: 'example.contract_user.renamed')
/// Event that renames a user implementing an interface target.
@OperationFor(type: UserContract, key: 'example.contract_user.renamed')
class ContractUserRenamed implements ContinuumEvent {
ContractUserRenamed({
required this.newDisplayName,
Expand Down
Loading