continuum_generator 4.1.0 copy "continuum_generator: ^4.1.0" to clipboard
continuum_generator: ^4.1.0 copied to clipboard

Code generator for the continuum event sourcing library.

Continuum Generator #

Code generator for the continuum event sourcing library. Automatically generates event handling code and discovers all aggregates in your project.

What It Generates #

Per-Aggregate Files (*.g.dart) #

For each @Aggregate() class, generates:

  1. Event handler mixin (_$YourAggregateEventHandlers)

    • Connects your applyEventName() methods to events
    • Type-safe event dispatching
  2. Extensions:

    • applyEvent() - Apply single event to aggregate
    • replayEvents() - Reconstruct aggregate from event stream
    • createFromEvent() - Factory for first event
  3. Registries (for persistence):

    • Event serialization registry
    • Aggregate factory registry
    • Event applier registry

Global Discovery (lib/continuum.g.dart) #

Automatically discovers all @Aggregate() classes and generates:

final List<GeneratedAggregate> $aggregateList = [
  $Account,
  $User,
  // ... all aggregates sorted alphabetically
];

This enables zero-configuration setup:

final store = EventSourcingStore(
  eventStore: myStore,
  aggregates: $aggregateList, // Just works!
);

Installation #

Add to your pubspec.yaml:

dependencies:
  continuum: latest

dev_dependencies:
  build_runner: ^2.4.0
  continuum_generator: latest

Usage #

1. Annotate Your Classes #

import 'package:continuum/continuum.dart';

part 'user.g.dart';

@Aggregate()
class User with _$UserEventHandlers {
  final String id;
  String email;

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

  static User createFromUserCreated(UserCreated event) {
    return User._(id: event.userId, email: event.email);
  }

  @override
  void applyEmailChanged(EmailChanged event) {
    email = event.newEmail;
  }
}

@AggregateEvent(of: User, type: 'user.created')
class UserCreated implements ContinuumEvent {
  UserCreated({
    required this.userId,
    required this.email,
    EventId? eventId,
    DateTime? occurredOn,
    Map<String, Object?> metadata = const {},
  }) : id = eventId ?? EventId.fromUlid(),
       occurredOn = occurredOn ?? DateTime.now(),
       metadata = Map<String, Object?>.unmodifiable(metadata);

  final String userId;
  final String email;

  @override
  final EventId id;

  @override
  final DateTime occurredOn;

  @override
  final Map<String, Object?> metadata;
  
  Map<String, dynamic> toJson() => {
    'userId': userId,
    'email': email,
    'eventId': id.toString(),
    'occurredOn': occurredOn.toIso8601String(),
    'metadata': metadata,
  };

  factory UserCreated.fromJson(Map<String, dynamic> json) {
    return UserCreated(
      userId: json['userId'] as String,
      email: json['email'] as String,
      eventId: EventId(json['eventId'] as String),
      occurredOn: DateTime.parse(json['occurredOn'] as String),
      metadata: Map<String, Object?>.from(json['metadata'] as Map),
    );
  }
}

@AggregateEvent(of: User, type: 'user.email_changed')
class EmailChanged implements ContinuumEvent {
  EmailChanged({
    required this.userId,
    required this.newEmail,
    EventId? eventId,
    DateTime? occurredOn,
    Map<String, Object?> metadata = const {},
  }) : id = eventId ?? EventId.fromUlid(),
       occurredOn = occurredOn ?? DateTime.now(),
       metadata = Map<String, Object?>.unmodifiable(metadata);

  final String userId;
  final String newEmail;

  @override
  final EventId id;

  @override
  final DateTime occurredOn;

  @override
  final Map<String, Object?> metadata;
  
  Map<String, dynamic> toJson() => {
    'userId': userId,
    'newEmail': newEmail,
    'eventId': id.toString(),
    'occurredOn': occurredOn.toIso8601String(),
    'metadata': metadata,
  };

  factory EmailChanged.fromJson(Map<String, dynamic> json) {
    return EmailChanged(
      userId: json['userId'] as String,
      newEmail: json['newEmail'] as String,
      eventId: EventId(json['eventId'] as String),
      occurredOn: DateTime.parse(json['occurredOn'] as String),
      metadata: Map<String, Object?>.from(json['metadata'] as Map),
    );
  }
}

2. Run Build Runner #

# One-time build
dart run build_runner build

# Watch mode (rebuilds on file changes)
dart run build_runner watch

# Clean previous builds
dart run build_runner build --delete-conflicting-outputs

3. Use Generated Code #

// Import your aggregate (with generated part)
import 'domain/user.dart';

// Import auto-discovered list
import 'continuum.g.dart';

void main() {
  // Zero-configuration setup!
  final store = EventSourcingStore(
    eventStore: InMemoryEventStore(),
    aggregates: $aggregateList,
  );

  final userId = StreamId('123');

  // Create + mutate within a session
  final session = store.openSession();
  session.startStream<User>(userId, UserCreated(userId, 'alice@example.com'));
  await session.saveChangesAsync();

  // Load aggregate (reconstructed from events)
  final readSession = store.openSession();
  final user = await readSession.loadAsync<User>(userId);
  print(user.email); // alice@example.com
}

Generated Code Structure #

Event Handler Mixin #

The generator creates a mixin that dispatches events to your apply methods:

// Generated in user.g.dart
mixin _$UserEventHandlers {
  void applyEmailChanged(EmailChanged event);
  
  void $applyEvent(ContinuumEvent event) {
    if (event is EmailChanged) return applyEmailChanged(event);
    throw UnknownEventException(event.runtimeType);
  }
}

Extension Methods #

// Generated in user.g.dart
extension UserEventSourcingExtensions on User {
  void applyEvent(ContinuumEvent event) {
    $applyEvent(event);
  }
  
  static User replayEvents(Iterable<ContinuumEvent> events) {
    // Replays events to reconstruct aggregate
  }
  
  static User createFromEvent(ContinuumEvent event) {
    // Calls User.createFromUserCreated() etc.
  }
}

Build Configuration #

Custom Configuration (Optional) #

Create build.yaml in your project root:

targets:
  $default:
    builders:
      continuum_generator:
        enabled: true
        options:
          # Options can be added here in future versions

Multiple Packages #

If you have multiple packages with aggregates, run build_runner in each:

# In package A
cd packages/domain_a
dart run build_runner build

# In package B  
cd ../domain_b
dart run build_runner build

Each package gets its own continuum.g.dart with its aggregates.

How Auto-Discovery Works #

The generator scans all .dart files in your lib/ directory for @Aggregate() annotations and collects them into $aggregateList. This happens in a separate build phase after all per-aggregate generators complete.

You don't need to:

  • Manually import aggregate files
  • Maintain a registry
  • Merge multiple registries

Just:

  1. Add @Aggregate() annotation
  2. Run build_runner
  3. Use $aggregateList

Troubleshooting #

"part 'file.g.dart' not found" #

Run the generator:

dart run build_runner build

"Undefined name '_$MyAggregateEventHandlers'" #

Make sure:

  1. You have part 'my_aggregate.g.dart'; directive
  2. Your class has the @Aggregate() annotation
  3. You've run build_runner

"No apply method found for event" #

Ensure your aggregate has a method like:

MyAggregate applyMyEvent(MyEvent event) { ... }

The method name must be apply + event class name.

Changes not reflected #

Try rebuilding with clean:

dart run build_runner build --delete-conflicting-outputs

Examples #

See the continuum package examples for complete usage examples.

Contributing #

See the repository for contribution guidelines.

License #

MIT

0
likes
140
points
66
downloads

Documentation

API reference

Publisher

verified publisherzooper.dev

Weekly Downloads

Code generator for the continuum event sourcing library.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

analyzer, bounded, build, continuum, glob, source_gen

More

Packages that depend on continuum_generator