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
76 changes: 76 additions & 0 deletions .github/instructions/dart.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
applyTo: '**/*.dart'
---

# General
- ALWAYS write clean, readable, maintainable, explicit code.
- ALWAYS write code that is easy to refactor and reason about.
- NEVER assume context or generate code that I did not explicitly request.
- ALWAYS name files after the primary class or functionality they contain.

# Documentation
- WHEN a CHANGELOG.md file is present in the project root, ALWAYS add a changelog entry for any non-trivial change.Minimal flat vector icon. Multiple small dots flowing left-to-right into one larger dot in a single line. Clean geometric style, blue palette, no text, transparent background.

- ALWAYS place a `///` library-level documentation block (before imports) ONLY on:
- lib/<package>.dart (the main public entrypoint)
- a small number of intentionally exposed public sub-libraries
- NEVER add library file-docs on internal files inside `src/`
- ALWAYS keep package-surface documentation concise, stable, and user-facing
- ALWAYS write Dart-doc (`///`) for:
- every class
- every constructor
- every public and private method
- every important field/property
- ALWAYS add inline comments INSIDE methods explaining **why** something is done (preferred) or **what** it does if unclear.
- NEVER generate README / docs / summary files unless explicitly asked.
- NEVER document example usage.

# Code Style
- ALWAYS use long, descriptive variable and method names. NEVER use abbreviations.
- ALWAYS use explicit return types — NEVER rely on type inference for public API surfaces.
- ALWAYS avoid hidden behavior or magic — explain reasons in comments.
- NEVER use `dynamic` unless explicitly requested.
- NEVER swallow exceptions — failures must be explicit and documented.

# Package Modularity
- ALWAYS organize code by feature or concept, NOT by layers (domain/app/infrastructure/etc.).
- ALWAYS keep related classes in the same folder to avoid unnecessary cross-navigation.
- ALWAYS aim for package-internal cohesion: a feature should be usable independently of others.
- NEVER introduce folders like `domain`, `application`, `infrastructure`, `presentation` inside a package unless explicitly asked.
- ALWAYS design APIs as small, composable, orthogonal units that can be imported independently.
- ALWAYS hide internal details using file-private symbols or exports from a single public interface file.
- ALWAYS expose only few careful public entrypoints through `package_name.dart`.
- NEVER expose cluttered API surfaces; keep users' imports short and predictable.

# Asynchronous / IO
- ALWAYS suffix async methods with `Async`.
- NEVER do IO inside constructors.
- ALWAYS document async side-effects.

# Constants
- NEVER implement magic values.
- ALWAYS elevate numbers, strings, durations, etc. to named constants.

# Assumptions
- IF details are missing, ALWAYS state assumptions **above the code** before writing it.
- NEVER introduce global state unless explicitly required.

# API Design
- ALWAYS think in terms of public API surface: every public symbol must be intentionally exposed and supported long-term.
- ALWAYS hide implementation details behind internal files.
- ALWAYS consider whether adding a type forces future backwards-compatibility.
- ALWAYS design for testability (stateless helpers, pure functions, injectable dependencies).

# Folder Hygiene
- NEVER create folders "just in case."
- ALWAYS delete dead code aggressively.
- ALWAYS keep `src/` readable even after 2 years of growth.

# Code Hygiene
- NEVER implement barrel export files.
- ALWAYS write code that compiles with ZERO warnings, errors, or analyzer hints.
- ALWAYS remove unused imports, unused variables, unused private fields, and unreachable code.
- ALWAYS prefer explicit typing to avoid inference warnings.
- ALWAYS mark classes, methods, or variables as `@visibleForTesting` or private when they are not part of the public API.
- NEVER ignore analyzer warnings with `// ignore:` unless explicitly asked.
- ALWAYS keep lint and style problems in VSCode Problems panel at ZERO, unless unavoidable and explicitly justified in comments.
28 changes: 28 additions & 0 deletions .github/instructions/dart.styling.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
applyTo: '**/*.dart'
---

# File Ordering (top → bottom)
- library documentation (ONLY when allowed)
- imports (dart: → package: → relative), alphabetical
- exports, alphabetical
- top-level constants
- top-level typedefs, aliases
- top-level public enums
- top-level public classes / mixins / extensions
- top-level private enums
- top-level private classes / mixins / extensions (ALWAYS LAST)

# Class Member Ordering
1. static fields (public → private)
2. instance fields (public → private)
3. constructors (public → named → private)
4. factory constructors
5. public getters
6. public setters
7. public methods
8. operator overloads
9. protected methods
10. private getters / setters
11. private methods
12. static methods (public → private)
43 changes: 43 additions & 0 deletions .github/instructions/dart.tests.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
applyTo: '**/*.dart'
---

# Tests
- ALWAYS write tests for EVERY publicly accessible class, function, and method.
- ALWAYS write tests with the primary goal of exposing possible bugs — NOT simply making tests pass.
- ALWAYS test failure cases, invalid input, unexpected state, and edge conditions.
- ALWAYS create exactly one unit test file per class being tested.
- ALWAYS name the test file `<class_name>_test.dart` or `<feature_name>_test.dart`.

# Unit Tests
- ALWAYS use Arrange–Act–Assert pattern with clear separation.
- ALWAYS write descriptive test names that explain expected behavior.
- ALWAYS add inline comments inside tests explaining WHY assertions matter.
- ALWAYS include tests for:
- Happy path behavior
- Error cases and thrown exceptions
- Boundary conditions
- Null / empty values where applicable
- Timing and concurrency behavior if async
- NEVER skip tests for private methods if they contain complex logic.
(If a private method is trivial, call it indirectly through public API instead.)
- WHEN a class depends on collaborators, ALWAYS use fakes or stubs — NEVER use real infrastructure in unit tests.

# Integration Tests (only when applicable)
- ALWAYS write integration tests to verify whole workflows that span multiple public classes.
- ALWAYS cover multi-step flows, IO boundaries, and dependency wiring.
- NEVER write integration tests when a unit test is sufficient.
- ALWAYS isolate integration tests into `test/integration/` and name according to workflow.

# Test Hygiene
- NEVER use random sleeps or timing hacks — use proper async waiting or dependency injection.
- NEVER rely on global order of test execution.
- ALWAYS ensure tests remain readable after years — avoid clever tricks or meta test logic.

# Mocks
- ALWAYS use Mockito for mocking dependencies.
- ALWAYS mock collaborators instead of creating real implementations in unit tests.
- ALWAYS generate mock classes via `build_runner` when needed.
- NEVER use real data sources, HTTP calls, or platform channels in unit tests.
- ALWAYS verify interactions on mocks when behavior depends on method-call side effects.
- ALWAYS keep mock usage minimal and focused — tests should assert behavior, not implementation details.
17 changes: 17 additions & 0 deletions .github/instructions/flutter.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
applyTo: '**/*.dart'
---

# Clean Architecture for Flutter apps
- ALWAYS separate responsibilities:
- domain/: entities, value objects, business rules
- application/: services, use-cases, orchestrators
- infrastructure/: concrete implementations, IO, APIs
- presentation/: Flutter widgets, controllers, adapters
- NEVER mix domain logic inside UI or infrastructure.
- NEVER inject `WidgetRef` or `Ref` into domain/application classes — ONLY resolve dependencies at provider boundaries.

# Flutter Widgets
- ALWAYS explain the purpose of a widget in Dart-doc.
- ALWAYS extract callbacks into named functions when possible.
- NEVER override themes or text styles unless explicitly requested.
29 changes: 29 additions & 0 deletions packages/raiser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- **BREAKING**: `RaiserEvent` is now a pure `abstract interface class` instead of a base class with implementation
- **Note: Using `RaiserEvent` is completely optional - `EventBus` works with ANY type**
- If you choose to use `RaiserEvent`, you must explicitly implement all three required properties: `id`, `occurredOn`, and `metadata`
- No more automatic property initialization - implement the interface explicitly
- This change allows events to use composition and multiple interfaces without inheritance constraints
- `RaiserEvent` now implements `ZooperDomainEvent` from `zooper_flutter_core` package
- Added `zooper_flutter_core: ^1.0.2` as an optional dependency (only needed if you use `RaiserEvent`)
- Ensures consistency across domain events in the Zooper ecosystem
- Event IDs in `RaiserEvent` now use `EventId` type (ULID-based) instead of `String`
- IDs are generated via `EventId.fromUlid()` for better uniqueness guarantees
- Access raw string value via `event.id.value` when needed
- Renamed `RaiserEvent.timestamp` property to `occurredOn` for clarity and DDD alignment
- Removed `aggregateId` as a direct property from `RaiserEvent`
- Store aggregate identifiers in the `metadata` map instead: `metadata: {'aggregateId': 'user-123'}`
- Access via `event.metadata['aggregateId'] as String?`
- Removed all helper mixins and convenience extensions
- No more `StandardRaiserEvent` mixin
- No more extension methods for `aggregateId` or `timestamp` accessors
- Events implementing `RaiserEvent` must be implemented explicitly following the interface pattern

### Important Note

**`EventBus` is fully generic and does not require `RaiserEvent`!** You can publish and subscribe to any type. The `RaiserEvent` interface is purely optional for users who want standardized domain event metadata.


## [2.0.1] - 2026-01-08

### Changed
Expand Down
91 changes: 60 additions & 31 deletions packages/raiser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,25 @@ A type-safe, async-first domain event library for Dart. Raiser provides a clean

```yaml
dependencies:
raiser: ^1.0.0
raiser: ^2.0.1
```

## Quick Start

**EventBus works with ANY type** - you don't need to use `RaiserEvent` at all:

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

// Define an event
class UserCreated extends RaiserEvent {
// Define an event - just a simple class!
final class UserCreated {
UserCreated({
required this.userId,
required this.email,
});

final String userId;
final String email;

UserCreated({required this.userId, required this.email});

@override
Map<String, dynamic> toMetadataMap() => {
...super.toMetadataMap(),
'userId': userId,
'email': email,
};
}

void main() async {
Expand All @@ -57,32 +55,63 @@ void main() async {
}
```

## Domain Events
## RaiserEvent (Optional)

**You don't have to use `RaiserEvent`!** The `EventBus` is fully generic and works with any type.

However, if you want standardized domain event metadata, you can optionally implement `RaiserEvent`, which is an interface extending `ZooperDomainEvent` from [zooper_flutter_core](https://pub.dev/packages/zooper_flutter_core). This provides:

All events extend `RaiserEvent`, which provides automatic metadata:
| Property | Type | Description |
|----------|------|-------------|
| `id` | `EventId` | Unique identifier (ULID-based) |
| `occurredOn` | `DateTime` | When the event occurred |
| `metadata` | `Map<String, Object?>` | Additional event context |

| Property | Description |
|----------|-------------|
| `id` | Unique identifier (auto-generated) |
| `timestamp` | Creation time (auto-captured) |
| `aggregateId` | Optional link to a domain aggregate |
`RaiserEvent` is intentionally an **interface** (not a base class) to avoid forcing single inheritance. If you choose to use it, implement it explicitly:

```dart
class OrderPlaced extends RaiserEvent {
import 'package:zooper_flutter_core/zooper_flutter_core.dart';

final class OrderPlaced implements RaiserEvent {
OrderPlaced({
required this.orderId,
required this.amount,
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 orderId;
final double amount;

OrderPlaced({required this.orderId, required this.amount, super.aggregateId});
@override
final EventId id;

@override
Map<String, dynamic> toMetadataMap() => {
...super.toMetadataMap(),
'orderId': orderId,
'amount': amount,
};
final DateTime occurredOn;

@override
final Map<String, Object?> metadata;
}
```

### Aggregate IDs

Store aggregate identifiers in the `metadata` map:

```dart
final event = OrderPlaced(
orderId: 'order-123',
amount: 99.99,
metadata: {'aggregateId': 'user-456'},
);

// Access via metadata
final aggregateId = event.metadata['aggregateId'] as String?;
```

## Event Handlers

### Function Handlers
Expand Down Expand Up @@ -126,14 +155,14 @@ Wrap handler execution with cross-cutting concerns like logging, timing, or vali

```dart
// Add middleware that wraps all handler execution
bus.addMiddleware((event, next) async {
bus.addMiddleware((Object event, Future<void> Function() next) async {
print('Before: ${event.runtimeType}');
await next();
print('After: ${event.runtimeType}');
}, priority: 100);

// Middleware with higher priority wraps those with lower priority
bus.addMiddleware((event, next) async {
bus.addMiddleware((Object event, Future<void> Function() next) async {
final stopwatch = Stopwatch()..start();
await next();
print('Took ${stopwatch.elapsedMilliseconds}ms');
Expand Down Expand Up @@ -198,12 +227,12 @@ For automatic handler discovery and registration, use the companion packages:

```yaml
dependencies:
raiser: ^1.0.0
raiser_annotation: ^1.0.0
raiser: ^2.0.1
raiser_annotation: ^2.0.1

dev_dependencies:
build_runner: ^2.4.0
raiser_generator: ^1.0.0
raiser_generator: ^2.0.1
```

See [raiser_generator](https://pub.dev/packages/raiser_generator) for details.
Expand Down
Loading